Skip to content
Charles Solar edited this page Feb 8, 2021 · 6 revisions

Testing Entities

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

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.

Note

context.Id is a special function which sets up a auto generated Id to make your testing easier. Instead of having to keep many var productId = Guid.NewGuid(); lines in your tests you should use context.Id(). Think of it like Moq.IsAny<Guid>() - you can safely generate streams without caring about the actual ids. Read more about context.Id below.

Exercise

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);

Check

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.

Checking Business Exceptions

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);
}

About context.Id()

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.

context.Id() is not magic

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.