Skip to content
Aaron Hanusa edited this page Feb 1, 2021 · 17 revisions

Peasy was designed from the ground up following the SOLID design principles. As a result, testing your classes that derive from and implement peasy classes and interfaces is easy.

This document provides guidance and examples of how to test your concrete implementations of peasy actors that should be tested. Note that these tests use a handy assertion framework called shouldly.

A word on unit testing styles

There is much debate around which stylistic unit tests are best, and essentially boils down to state-based vs. behavior-based unit tests. There are pros and cons to each approach and these will not be covered in this documentation.

With regards to state-based testing, you can create an in-memory data proxy project and use your concrete in-memory proxies in your unit tests. To save you time, you can use an abstract base class located in the Peasy.DataProxy.InMemory project. This class can be downloaded and added to your project, or by installed via the Nuget package. More details can be found on the project website.

With regards to behavior-based unit testing, there are some really excellent tools in the .NET testing ecosystem, including Moq, RhinoMocks, and FakeItEasy to name a few.

And of course you can always use state-based and behavior-based unit tests in conjunction with each other.

Testing Rules

Given the following business rule ...

public class CustomerAgeVerificationRule : RuleBase
{
    private DateTime _birthDate;

    public CustomerAgeVerificationRule(DateTime birthDate)
    {
        _birthDate = birthDate;
    }

    // Synchronous support
    protected override void OnValidate()
    {
        if ((DateTime.Now.Year - _birthDate.Year) < 18)
        {
            Invalidate("New users must be at least 18 years of age");
        }
    }

    // Asynchronous support
    protected override async Task OnValidateAsync()
    {
        OnValidate();
    }
}

And testing it out...

var rule = new CustomerAgeVerificationRule(DateTime.Now.AddYears(-17));
rule.Validate().IsValid.ShouldBe(false);
rule.Validate().ErrorMessage.ShouldBe("New users must be at least 18 years of age");

var rule = new CustomerAgeVerificationRule(DateTime.Now.AddYears(-18));
rule.Validate().IsValid.ShouldBe(true);
rule.Validate().ErrorMessage.ShouldBe(null);

Testing Commands

In the following example, we cover the implementation of the DeleteOrderCommand, which is responsible for deleting an order and all of its associated order items. A full implementation of the command can be located here.

For brevity sake, we will just cover the execution method of the command, leaving out the business rule coverage.

protected override void OnExecute()
{
    _transactionContext.Execute(() =>
    {
        _orderDataProxy.Delete(_orderID);
        CurrentOrderItems.ForEach(i =>
        {
            _orderItemService.DeleteCommand(i.ID).Execute();
        });
    });
}

In this example, the deletion of an order and its associated order items are invoked within a transaction context.

Let's take a look at how we might add code coverage around this functionality. A full implementation of all of the tests for this class can be located here.

State-Based Testing Example

[TestMethod]
public void Execution_deletes_order_and_associated_order_items_state_based()
{
    var order = new Order() { OrderDate = DateTime.Now };
    var orderRepo = new OrderRepository(Mock.Of<ICustomerDataProxy>(), Mock.Of<IOrderItemDataProxy>());
    orderRepo.Clear();
    order = orderRepo.Insert(order);
    var orderItemRepo = new OrderItemRepository();
    orderItemRepo.Clear();
    orderItemRepo.Insert(new OrderItem { OrderID = order.ID, OrderStatusID = OrderStatusConstants.PENDING_STATUS });
    orderItemRepo.Insert(new OrderItem { OrderID = order.ID, OrderStatusID = OrderStatusConstants.SUBMITTED_STATUS });
    orderItemRepo.Insert(new OrderItem { OrderID = order.ID, OrderStatusID = OrderStatusConstants.BACK_ORDERED_STATE });
    orderItemRepo.Insert(new OrderItem { OrderID = 2, OrderStatusID = OrderStatusConstants.PENDING_STATUS });
    var orderItemService = new OrderItemService(orderItemRepo, Mock.Of<IProductDataProxy>(), Mock.Of<IInventoryItemDataProxy>(), new MockTransactionContext());

    var command = new DeleteOrderCommand(order.ID, orderRepo, orderItemService, new MockTransactionContext());
    command.Execute();
    orderRepo.GetAll().ShouldBeEmpty();
    orderItemRepo.GetAll().Count().ShouldBe(1);
}

In this example, we create a state-based unit test by consuming in-memory data proxy implementations and ensuring that they contain the correct record counts after invoking the command.

Behavior-Based Testing Example

[TestMethod]
public void Execution_deletes_order_and_associated_order_items()
{
    var orders = new List<Order>()
    {
        new Order { ID = 1 }
    };
    var orderItems = new List<OrderItem>
    {
        new OrderItem { ID = 1, OrderID = 1, OrderStatusID = OrderStatusConstants.PENDING_STATUS },
        new OrderItem { ID = 2, OrderID = 1, OrderStatusID = OrderStatusConstants.SUBMITTED_STATUS },
        new OrderItem { ID = 3, OrderID = 1, OrderStatusID = OrderStatusConstants.BACK_ORDERED_STATE },
        new OrderItem { ID = 4, OrderID = 2, OrderStatusID = OrderStatusConstants.BACK_ORDERED_STATE }
    };
    var deletedOrderItemIds = new List<long>();
    var orderID = 1;
    var orderDataProxy = new Mock<IOrderDataProxy>();
    orderDataProxy.Setup(p => p.Delete(orderID)).Callback((long id) => orders.Remove(id));
    var orderItemDataProxy = new Mock<IOrderItemDataProxy>();
    orderItemDataProxy.Setup(p => p.GetByOrder(It.IsAny<long>()))
                      .Returns((long i) => orderItems.Where(item => item.OrderID == i));
    orderItemDataProxy.Setup(p => p.GetByID(It.IsAny<long>()))
                      .Returns((long i) => orderItems.First(oi => oi.ID == i));
    orderItemDataProxy.Setup(p => p.Delete(It.IsAny<long>())).Callback((long id) => deletedOrderItemIds.Add(id));
    var command = new DeleteOrderCommand
                      (
                          orderID,
                          orderDataProxy.Object,
                          new OrderItemService
                          (
                              orderItemDataProxy.Object,
                              Mock.Of<IProductDataProxy>(),
                              Mock.Of<IInventoryItemDataProxy>(),
                              Mock.Of<ITransactionContext>()
                          ),
                          new MockTransactionContext()
                      );
    var result = command.Execute();
    result.Success.ShouldBe(true);
    orders.Count().ShouldBe(0);
    deletedOrderItemIds.ShouldBe(new long[] { 1, 2, 3 });
}

In this example, we create a behavior-based unit test by ensuring that specific methods are invoked in a specific order.

Testing Service Commands

Coming Soon...