-
Notifications
You must be signed in to change notification settings - Fork 60
Testing
The Aggregates.NET.Testing
package contains hooks into our internal processes to make testing your handlers and entities easier. Coupled with NServiceBus.Testing - you'll be able to run tests on your business logic without having to connect to eventstore, rabbit, learning transport, or anything.
There are tests using this package in the samples folder - specifically:
To test your handlers you'll need to implement three phases:
- Plan
- Exercise
- Check
Planning involves defining the streams you want to exist when running the command you want to exercise. Typically this means you define the state of the entities before the command so as to trigger the right behavior when the command is executed.
Here is an example:
context.UoW.Plan<Product>(context.Id())
.HasEvent<Events.Added>(x =>
{
x.ProductId = context.Id();
x.CatalogBrandId = context.Id();
x.CatalogTypeId = context.Id();
x.Name = "test";
x.Price = 100;
});
Here we tell the test context that a Product
exists of any id - and it has an event Events.Added
with the specific properties.
context.Id
is a special function which sets up a auto generated Id to make your testing easier. Instead of having to keep manyvar productId = Guid.NewGuid();
lines in your tests you should usecontext.Id()
. Think of it likeMoq.IsAny<Guid>()
- you can safely generate streams without caring about the actual ids. Read more aboutcontext.Id
below.
When your command handler loads Product
it will receive an entity instance with this 1 event hydrated. We're going to test a command ChangePrice
- the entity is defined like so:
class Product : Entity<Product, State> {
private Product() {}
public void ChangePrice(int newPrice) {
if((newPrice / State.Price) >= 2) {
throw new BusinessException("Cannot increase the product price by more than 200%!");
}
Apply<Events.PriceChanged>(x => {
x.ProductId = Id;
x.OldPrice = State.Price;
x.NewPrice = newPrice;
});
}
}
You would exercise this action with
await handler.Handle(new Commands.ChangePrice
{
ProductId = context.Id(),
Price = 150
}, context).ConfigureAwait(false);
Finally - you can check to see what events were raised while processing the command. To do this in our product sample here you'd do
context.UoW().Check<Product>(context.Id()).Raised<Events.PriceChanged>();
This would simply check that the single event was raised. You can also do more specific checks with the eventstream by supplying an assertion
context.UoW().Check<Product>(context.Id()).Raised<Events.PriceChanged>(x => x.NewPrice == 150);
Or alternatively you can specify the exact event you desired to be raised
context.UoW().Check<Product>(context.Id()).Raised<Events.PriceChanged>(x => {
x.ProductId = context.Id();
x.NewPrice = 150;
x.OldPrice = 100;
});
Doing this will ensure a copy of that exact event was raised.
As I wrote a business exception case in the product here is an example of a test that covers our business logic.
[Theory, AutoFakeItEasyData]
public async Task Should_not_allow_200_percent_increase(
TestableContext context,
Handler handler
)
{
context.UoW.Plan<Product>(context.Id())
.HasEvent<Events.Added>(x =>
{
x.ProductId = context.Id();
x.CatalogBrandId = context.Id();
x.CatalogTypeId = context.Id();
x.Name = "test";
x.Price = 100;
});
await Assert.ThrowsAsync<BusinessException>(() => handler.Handle(new Commands.ChangePrice
{
ProductId = context.Id(),
Price = 50000
}, context)).ConfigureAwait(false);
}
context.Id
will quickly become your best friend. This method generates an Id object which you can use as a Guid, string, or long Id for your entities, commands, and events. context.Id
solves the problem of having to code ids into your tests. Without context.Id
you would have to keep track of entity ids in your test like so
var productId = Guid.NewGuid();
var brandId = Guid.NewGuid();
var typeId = Guid.NewGuid();
context.UoW.Plan<Product>(productId)
.HasEvent<Events.Added>(x =>
{
x.ProductId = productId;
x.CatalogBrandId = brandId;
x.CatalogTypeId = typeId;
x.Name = "test";
x.Price = 100;
});
await handler.Handle(new Commands.ChangePrice
{
ProductId = productId,
Price = 150
}, context).ConfigureAwait(false);
context.UoW().Check<Product>(productId).Raised<Events.PriceChanged>(x => {
x.ProductId = productId;
x.NewPrice = 150;
x.OldPrice = 100;
});
We can avoid all that NewGuid nastiness by letting the testing framework generate your ids. There are a couple of things to know about context.Id
though.
By default context.Id()
will generate a generic id for the entity type which will be used every time the entity type is read. It's usually what you'd use all over but its important to note that context.Id()
will return the same Id from every call.
Your generated id will be the same for every call to context.Id()
var id1 = context.Id();
var id2 = context.Id();
id1.ToString();
>>> {00000000-0000-0000-0000-000000000001}
id2.ToString();
>>> {00000000-0000-0000-0000-000000000001}
This is usually OK because you are only reading max 1 entity type for each test - but for more complicated situations you will need something a little more robust.
Take the following test we want to implement:
// define product 1
context.UoW.Plan<Product>(context.Id())
.HasEvent<Events.Added>(x =>
{
x.ProductId = context.Id();
x.CatalogBrandId = context.Id();
x.CatalogTypeId = context.Id();
x.Name = "Product One";
x.Price = 100;
});
// define product 2
context.UoW.Plan<Product>(context.Id())
.HasEvent<Events.Added>(x =>
{
x.ProductId = context.Id();
x.CatalogBrandId = context.Id();
x.CatalogTypeId = context.Id();
x.Name = "Product Two";
x.Price = 200;
});
await handler.Handle(new Commands.ObsoleteProductLine
{
RetireProductId = context.Id(),
ReplacementProductId = context.Id()
}, context).ConfigureAwait(false);
context.UoW().Check<Product>(context.Id()).Raised<Events.Obsolete>(x => x.ReplacementProductId == context.Id());
There is no way for context.Id()
to know its being generated for different properties so your command handler will attempt to load two different products and receive the same exact product.
We fix this by using labeled Ids - with context.Id("label")
. By supplying a parameter to the Id method you can generate a keyed Id which you can recall at any time during the test. Here is the same test fixed to use labeled Ids:
// define product 1
context.UoW.Plan<Product>(context.Id("product1"))
.HasEvent<Events.Added>(x =>
{
x.ProductId = context.Id("product1");
x.CatalogBrandId = context.Id();
x.CatalogTypeId = context.Id();
x.Name = "Product One";
x.Price = 100;
});
// define product 2
context.UoW.Plan<Product>(context.Id("product2"))
.HasEvent<Events.Added>(x =>
{
x.ProductId = context.Id("product2");
x.CatalogBrandId = context.Id();
x.CatalogTypeId = context.Id();
x.Name = "Product Two";
x.Price = 200;
});
await handler.Handle(new Commands.ObsoleteProductLine
{
RetireProductId = context.Id("product1"),
ReplacementProductId = context.Id("product2")
}, context).ConfigureAwait(false);
context.UoW().Check<Product>(context.Id("product1")).Raised<Events.Obsolete>(x => x.ReplacementProductId == context.Id("product2"));
context.Id(1)
is also valid - labels can be strings or integers.