通过单元测试学习 EventSourceDB

目录

介绍

背景

使用代码

第一次测试

第一个测试套件

检测否定情况

压力测试

兴趣点


介绍

关于事件溯源和 EventStoreDB,有很多白皮书和教程。假设你已经完成了这些操作,本文将提供一些集成测试来说明原始事件溯源持久存储(如EventStoreDb)的运行时行为,并提供了一些用于学习的湿材料。

引用:

背景

我第一次听说类似事件溯源的东西是90年代后期的“大数据”和Google搜索数据库,它们只允许附加数据,不允许在物理数据存储时删除或更新,用于良好的商业案例:性能和扩展,以及良好的商业环境:丰富的数字存储。

作为程序员,EventStoreDB或类似的东西并不是你离不开的东西,但是,它可能会让你更舒适地生活,尤其是对于“事件溯源的12个变革性好处”。

使用代码

首先,转到 EventStoreDB的开发者门户,并安装本地DB服务器和.NET客户端API(截至 2024年5月的 v24.2)。安装后,您应该能够看到以下内容:

第一次测试

/// <summary>
/// Basic example from EventSourceDb tutorial on https://developers.eventstore.com/clients/grpc/#creating-an-event
/// </summary>
/// <returns></returns>
[Fact]
public async Task TestBasic()
{
    var evt = new TestEvent
    {
        EntityId = Guid.NewGuid(),
        ImportantData = "I wrote my first event!"
    };

    var eventData = new EventData(
        Uuid.NewUuid(),
        "TestEvent",
        JsonSerializer.SerializeToUtf8Bytes(evt)
    );

    const string connectionString = "esdb://admin:changeit@localhost:2113?tls=true&tlsVerifyCert=false";
    /// tls should be set to true. Different from the official tutorial as of 2024-05-05 on https://developers.eventstore.com/clients/grpc/#creating-an-event. 
    /// I used the zipped EventStoreDb installed in Windows machine, launched with `EventStore.ClusterNode.exe --dev`

    var settings = EventStoreClientSettings.Create(connectionString);
    using EventStoreClient client = new(settings);
    string streamName = "some-stream";
    IWriteResult writeResult = await client.AppendToStreamAsync(
        streamName,
        StreamState.Any,
        new[] { eventData }
        );

    EventStoreClient.ReadStreamResult readStreamResult = client.ReadStreamAsync(
        Direction.Forwards,
        streamName,
        StreamPosition.Start,
        10);

    ResolvedEvent[] events = await readStreamResult.ToArrayAsync();
    string eventText = System.Text.Encoding.Default.GetString(events[0].Event.Data.ToArray());
    TestEvent eventObj = JsonSerializer.Deserialize<TestEvent>(eventText);
    Assert.Equal("I wrote my first event!", eventObj.ImportantData);
}

正如你所看到的,官方教程并不是开箱即用的,至少EventSourceDb Windows版本中是这样。因此,在对R/W操作进行第一次测试后,我有信心进行更多探索。

备注/提示:

  • 本文中介绍的测试用例/事实并不是真正的单元测试,因为我只是在学习过程中使用基于单元测试框架构建的测试套件来探索EventSourceDB的功能。但是,此类测试套件在商业应用程序开发中可能很有用,因为当应用程序代码中的某些内容出错并且相应的调用堆栈可能涉及外部依赖项(如EventSourceDb)时,您可以有一个简单的测试平台来探索和调试。

第一个测试套件

从EventSourceDb v21开始,API协议已从“RESTful”Web API更改为gRPC。并且通常建议重用gRPC连接。因此,设计了IClassFixture的EventStoreClientFixture。

public class EventStoreClientFixture : IDisposable
{
    // todo: What is the best way to clear an event store for unit tests? https://github.com/EventStore/EventStore/issues/1328
    public EventStoreClientFixture()
    {
        IConfigurationRoot config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
        string eventStoreConnectionString = config.GetConnectionString("eventStoreConnection");
        var settings = EventStoreClientSettings.Create(eventStoreConnectionString);
        Client = new(settings);
    }

    public EventStoreClient Client { get; private set; }

    #region IDisposable pattern
    bool disposed = false;

    void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                Client.Dispose();
            }

            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }
    #endregion
}

对于继承自IClassFixture<EventStoreClientFixture>的每个测试类,xUnit.NET运行时将执行一次夹具代码,因此同一类中的测试用例可能共享相同的EventStoreClient或gRPC连接。

但是,请记住,使用xUnit时,每个测试/事实都在测试类的新实例中执行,也就是说,如果一个测试类包含10个事实,则该类可以实例化10次。

public class BasicFacts : IClassFixture<EventStoreClientFixture>
{
    public BasicFacts(EventStoreClientFixture fixture)
    {
        eventStoreClient = fixture.Client; // all tests here shared the same client connection
    }

    readonly EventStoreClient eventStoreClient;

    [Fact]
    public async Task TestBackwardFromEnd()
    {
        string importantData = "I wrote my test with fixture " + DateTime.Now.ToString("yyMMddHHmmssfff");
        var evt = new TestEvent
        {
            EntityId = Guid.NewGuid(),
            ImportantData = importantData,
        };

        var eventData = new EventData(
            Uuid.NewUuid(),
            "testEvent", //The name of the event type. It is strongly recommended that these use lowerCamelCase, if projections are to be used.
            JsonSerializer.SerializeToUtf8Bytes(evt), // The raw bytes of the event data.
            null, // The raw bytes of the event metadata.
            "application/json" // The Content-Type of the EventStore.Client.EventData.Data. Valid values are 'application/json' and 'application/octet-stream'.
        );

        string streamName = "some-stream2";
        IWriteResult writeResult = await eventStoreClient.AppendToStreamAsync(
            streamName,
            StreamState.Any,
            new[] { eventData }
            );

        EventStoreClient.ReadStreamResult readStreamResult = eventStoreClient.ReadStreamAsync(
            Direction.Backwards,
            streamName,
            StreamPosition.End,
            10);
        Assert.Equal(TaskStatus.WaitingForActivation, readStreamResult.ReadState.Status);

        ResolvedEvent[] events = await readStreamResult.ToArrayAsync();
        string eventText = System.Text.Encoding.Default.GetString(events[0].Event.Data.ToArray());
        TestEvent eventObj = JsonSerializer.Deserialize<TestEvent>(eventText);
        Assert.Equal(importantData, eventObj.ImportantData); // so the first in events returned is the latest in the DB side.
    }
...
...
...

从运行中可以看出,与本地托管的EventSourceDb服务器的初始连接可能需要2秒以上,并且后续调用速度很快。

检测否定情况

对于现实世界的应用程序,编译代码并使其功能特性正常工作远远不足以为客户提供业务价值。为了防御性编程,主动测试否定情况并设置基本的防线以提供良好的用户体验,这一点很重要。

/// <summary>
/// Test with a host not existing
/// https://learn.microsoft.com/en-us/aspnet/core/grpc/deadlines-cancellation
/// </summary>
/// <returns></returns>
[Fact]
public async Task TestUnavailableThrows()
{
    var evt = new TestEvent
    {
        EntityId = Guid.NewGuid(),
        ImportantData = "I wrote my first event!"
    };

    var eventData = new EventData(
        Uuid.NewUuid(),
        "TestEvent",
        JsonSerializer.SerializeToUtf8Bytes(evt)
    );

    const string connectionString = "esdb://admin:changeit@localhost:2000?tls=true&tlsVerifyCert=false"; // this connection is not there on port 2000
    var settings = EventStoreClientSettings.Create(connectionString);
    using EventStoreClient client = new(settings);
    string streamName = "some-stream";
    var ex = await Assert.ThrowsAsync<Grpc.Core.RpcException>(() => client.AppendToStreamAsync(
        streamName,
        StreamState.Any,
        new[] { eventData }
        ));
    Assert.Equal(Grpc.Core.StatusCode.Unavailable, ex.StatusCode);
}

/// <summary>
/// Simulate a slow or disrupted connection to trigger error.
/// </summary>
/// <returns></returns>
[Fact]
public async Task TestDeadlineExceededThrows()
{
    var evt = new TestEvent
    {
        EntityId = Guid.NewGuid(),
        ImportantData = "I wrote my first event!"
    };

    var eventData = new EventData(
        Uuid.NewUuid(),
        "TestEvent",
        JsonSerializer.SerializeToUtf8Bytes(evt)
    );

    string streamName = "some-stream";
    var ex = await Assert.ThrowsAsync<Grpc.Core.RpcException>(() => eventStoreClient.AppendToStreamAsync(
        streamName,
        StreamState.Any,
        new[] { eventData },
        null,
        TimeSpan.FromMicroseconds(2) // set deadline very short to trigger DeadlineExceeded. This could happen due to network latency or TCP/IP's nasty nature.
        ));
    Assert.Equal(Grpc.Core.StatusCode.DeadlineExceeded, ex.StatusCode);
}

/// <summary>
/// 
/// </summary>
/// <returns></returns>
[Fact]
public async Task TestReadNotExistingThrows()
{
    EventStoreClient.ReadStreamResult readStreamResult = eventStoreClient.ReadStreamAsync(
        Direction.Backwards,
        "NotExistingStream",
        StreamPosition.End,
        10);
    Assert.Equal(TaskStatus.WaitingForActivation, readStreamResult.ReadState.Status);

    var ex = await Assert.ThrowsAsync<EventStore.Client.StreamNotFoundException>(async () => { var rs = await readStreamResult.ToArrayAsync(); });
    Assert.Contains("not found", ex.Message);
}

熟悉此类错误场景将帮助您在应用代码中应用防御性编程,并设计整体容错能力。

压力测试

当然,单元测试框架并不是压力测试或基准测试的非常好的测试平台,但是,它可能足以粗略地了解“事实”的运行速度。因此,如果相同的“事实”变得非常慢,则可能表明某些功能的不断发展的实现中存在错误。

以下事实并不是真正的单元测试或集成测试,以探索EventSourceDB的基本性能。

[Fact]
public async Task TestBackwardFromEndWriteOnly_100()
{
    for (int i = 0; i < 100; i++)
    {
        string importantData = "I wrote my test with fixture " + DateTime.Now.ToString("yyMMddHHmmssfff");
        var evt = new TestEvent
        {
            EntityId = Guid.NewGuid(),
            ImportantData = importantData,
        };

        var eventData = new EventData(
            Uuid.NewUuid(),
            "testEventStress",
            JsonSerializer.SerializeToUtf8Bytes(evt),
            null,
            "application/json"
        );

        string streamName = "some-streamStress";
        IWriteResult writeResult = await eventStoreClient.AppendToStreamAsync(
            streamName,
            StreamState.Any,
            new[] { eventData }
            );

        Assert.True(writeResult.LogPosition.CommitPosition > 0); 
    }
}

言论:

  • 如果涉及某些CI/CD管道,则可能需要从管道中排除这些测试用例。
     

兴趣点

EventStoreDb利用long(64位有符号)和ulong(64位无符号),看看JavaScript客户端如何处理和克服53位精度限制会很有趣。请继续关注,因为在接下来的几周里,我可能会添加更多关于事件溯源和EventStoreDb的文章:

  • 与EventSourceDb通信的TypeScript代码
  • 通过事件溯源进行审计跟踪
  • 通过事件溯源进行时间旅行

这些文章将使用集成测试套件来保护应用代码,并说明涉及EventSourceDB的功能特性的运行时行为。

引用:

https://www.codeproject.com/Articles/5381844/Learn-EventSourceDB-through-Unit-Testing

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值