A series of articles about event sourcing, EventSourceDB and real world scenarios
Introduction
About Event Sourcing and EventStoreDB, there are plenty of white papers and tutorials around. Presuming that you have walked through those, this article presents some integration tests to illustrate the runtime behaviours of a raw event sourcing persistent store like EventStoreDb, providing some wet materials for studying.
References:
Background
The first time I had heard of something similar to event sourcing was "Big Data" and Google search DB in late 90s, which allow only data appending, no deletion or update upon physical data storage, for good business cases: performance and scaling, and good business contexts: the abundance of digital storage.
EventStoreDB or alike is not something you as a programmer can't live without, however, it may make you live more comfortably falling into the Pit of Success, especially for "12 Transformative Benefits of Event Sourcing".
Using the code
Firstly go to EventStoreDB's developer portal, and install a local DB server and .NET client API (v24.2 as of May 2024). After the installation, you should be able to see the following:
The First Test
[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";
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);
}
As you can see, the official tutorial was not working out of the box, at least with the EventSourceDb Windows release. So having the first test with R/W operations, I have become confident in exploring more.
Remarks/Hints:
- The test cases/facts presented in this article are not really of unit testing, as I am just using the test suites built on a unit testing framework to explorer the functionality of EventSourceDB during learning. However, such test suite could be useful in a commercial app development since you could have a simple testbed to explore and debug when something in your app codes has gone wrong and the respective call stack may involve external dependency like EventSourceDb.
The First Test Suite
Since EventSourceDb v21, the API protocol had changed from "RESTful" Web API to gRPC. And it is generally recommended to reuse a gRPC connection. Therefore, EventStoreClientFixture for IClassFixture is crafted.
public class EventStoreClientFixture : IDisposable
{
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
}
The fixture codes will be executed by xUnit.NET runtime once for each test class inherited from IClassFixture<EventStoreClientFixture>, therefore test cases in the same class may share the same EventStoreClient or gRPC connection.
However, please keep in mind, with xUnit, each test/fact is executed in a new instance of the test class, that is, if a test class contains 10 facts, the class may be instantiated 10 times.
public class BasicFacts : IClassFixture<EventStoreClientFixture>
{
public BasicFacts(EventStoreClientFixture fixture)
{
eventStoreClient = fixture.Client;
}
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",
JsonSerializer.SerializeToUtf8Bytes(evt),
null,
"application/json"
);
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);
}
...
...
...
As you can see from the run, the initial connection to a local hosted EventSourceDb server may take over 2 seconds, and the subsequent calls are fast.
Test Negative Cases
For real world applications, having the codes compiled and having the functional features working are far from enough from delivering business values to the customers. It is important to proactively test negative cases for the sake of defensive programming and setup basic defence lines for delivering decent User Experience.
[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";
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);
}
[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)
));
Assert.Equal(Grpc.Core.StatusCode.DeadlineExceeded, ex.StatusCode);
}
[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);
}
Getting familiar with such error scenarios will help you to apply defensive programming in your app codes and design fault tolerance overall.
Stress Tests
Surely a unit testing framework is not a very good testbed for stress testing or benchmarking, however, it could be good enough to obtain rough idea about how fast a "fact" could run. So if the same "fact" become dramatically slower, it may signal something wrong in the evolving implementation of some functions.
The following facts are not really unit testing or integration testing to explore the basic performance of 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);
}
}
Remarks:
- If some CI/CD pipeline is involved, you may want to exclude these test cases from the pipeline.
Points of Interest
EventStoreDb utilizes long
(64-bit signed) and ulong
(64-bit unsigned), it will be interesting to see how a JavaScript client could handle and overcome the 53-bit precision limit. Please stay tuned, as in next few weeks I may be adding a few more articles about Event Sourcing and EventStoreDb:
- TypeScript Codes Talking to EventSourceDb
- Audit Trail through Event Sourcing
- Time Travel through Event Sourcing
These articles will be using integration test suites to guard the app codes and illustrate the runtime behaviours of functional features involving EventSourceDB.
References: