In the last post we covered an easy template to follow to create any unit test: Arrange, Act, Assert.
In this post, we dive into practical use of libraries that will make our unit testing lives easier 😁
Framework What?
A source of developer anxiety comes from unfamiliarity with the tools. Developers want to write unit tests, but they get entangled with the details of how to use the various frameworks available.
The following is a prescriptive list of tools you can use to start your unit testing journey. There are many more, but by sticking to these tools you’ll be on your way to becoming a Testing Ninja!
First, Some Terminology
Before we dive into the frameworks, let’s go over terminology you will encounter during unit testing:
- An SUT is the component whose behavior is being tested. This stands for “system under test”. Most likely it is a class instance from your application you will be testing.
- A fixture is a test environment that allows us to test the SUT in a controlled way. We will be going over AutoFixture to setup our test environment.
- A mock is an object whose behavior or properties are pre-defined. In most tests, we mock external dependencies. Classes that define behavior with a datastore, REST, GraphQL, or other external data source are mocked. We will use Moq for mock creation.
- A spy is an extension of a mock. A spy has methods that allow us to verify how it was called by the SUT. For example, we may want to verify that an
OrderRepository
’sCreateOrderAsync
method was called once during the test. Again, we will use Moq for spies. - An assertion is the actual test. We ensure that our SUT’s behavior is correct by making assertions on the output, and in some instances on called behavior of our spies. By making an assertion, you test that the actual result matches the expected result. Unit test runners already come with assertion libraries, but we will look at Shouldly as a way to make human-readable assertions.
Okay enough with the lecture. Onward!
xUnit
The core framework of your unit test suite should be a unit test runner. It runs all your unit tests.
xUnit is a no-frills framework that is designed to keep the developer in the guard rails of unit testing. It defines a unit test as either one of a Fact
or Theory
.
A Fact
defines a single, stand-alone unit test. It can be used if you want to test behavior with a single input.
A Theory
defines a parameterized unit test. Use this when you want to test behavior using multiple inputs.
[Fact]
public void ShouldAddNumbersCorrectly()
{
// Arrange
var x = 1;
var y = 2;
var expectedResult = 3;
// Act
var actualResult = x + y;
// Assert
Assert.Equal(expectedResult, actualResult);
}
[Theory]
[InlineData(0, 5)]
[InlineData(1, 4)]
[InlineData(2, 3)]
public void ShouldAddAllNumbersCorrectly(int x, int y)
{
// Arrange
var expectedResult = 5;
// Act
var actualResult = x + y;
// Assert
Assert.Equal(expectedResult, actualResult);
}
There are other test runners that can be used. They are MSTest and NUnit. I’ll do a comparison of all three in a future post.
AutoFixture
AutoFixture is an amazing library that provides multiple benefits:
- It provides the fixture for the test. A fixture is a stable environment from which the test can be done. AutoFixture provides functionality to create and maintain objects needed for unit testing.
- It is a test data factory. We can use AutoFixture to create test data for our SUT and mocks.
- It can integrate with Moq. AutoFixture can be the provider of mocks for our test. This can be used when creating our actual SUT. If we use the fixture to create it, then AutoFixture will automatically inject the SUT’s dependencies with mocks.
Before discovering AutoFixture, the pattern employed by me consisted of maintaining separate object factory classes to generate test data for the SUT. I also had to create mocks manually, and inject them on SUT creation. With AutoFixture, I can now eliminate all those custom object factories. I can use syntactic sugar to arrange my test properly. It makes test setup much easier.
Moq
The Moq library allows us to create mocks and spies in our unit tests. According to its Github page, it “is the only mocking library for .NET developed from scratch to take full advantage of .NET Linq expression trees and lambda expressions, which makes it the most productive, type-safe and refactoring-friendly mocking library available.” Wow!
You want to use mocks when you need to define the behavior of a dependency within your class. A common usage is mocking a repository interface. If your class uses a repository to query for data, then you can mock the interface’s methods to return expected objects, or throw an exception.
Use Moq when you need to create mocks, stubs, and spies for your tests. Here is an example.
// Create the mock
var serviceMock = new Mock<IService>();
// Define mock behavior
serviceMock.Setup(m => m.SomeServiceMethod(It.IsAny<int>())).Returns(aValue);
// Define a stub behavior
var validInputId = 1;
serviceMock.Setup(m => m.SomeServiceMethod(validInputId)).Returns(expectedValue);
// Act - perform the actual test
// ...
// Verify spy behavior
serviceMock.Verify(m => m.SomeServiceMethod(It.IsAny<int>()), Times.AtLeastOnce());
serviceMock.Verify(m => m.SomeServiceMethod(validInputId), Times.Once());
Shouldly
Shouldly takes assertions to the next level.
Not only does it make your code more readable, but the error messages that Shouldy prints makes it easy to read unit test output.
Let’s substitute our Assert
statements for Shouldly
statements in our last example.
[Fact]
public void ShouldAddNumbersCorrectly()
{
// Arrange
var x = 1;
var y = 1; // Will cause the unit test to fail
var expectedResult = 3;
// Act
var actualResult = x + y;
// Assert
actualResult.ShouldBe(expectedResult);
}
The previous Assert
statement would print this on error:
Assert.Equal() Failure
Expected: 3
Actual: 2
Shouldly
prints this error:
Shouldly.ShouldAssertException : actualResult should be 2 but was 3
This becomes more useful when you are doing test-driven development, or are doing more complex assertions.
Dissection of a Unit Test
Now it’s time to put it all together! Let’s walk through a unit test. Consider the following example code and it’s associated unit test.
Code
public async Task<Order> CreateOrderAsync(Order order)
{
_validator.ValidateAndThrow(order);
order.Amount = order.Price * order.Quantity;
order = await _orderTypeRepository.EnrichOrderTypeFieldAsync(order);
order = await _orderRepository.CreateOrderAsync(order);
return order;
}
Unit Test
[Fact]
public async void ShouldCreateOrder()
{
// Arrange
// #1
var fixture = new Fixture();
// #2
fixture.Customize(new AutoMoqCustomization());
// #3
var request = fixture.Build<Order>()
.Without(o => o.ID)
.Create();
// #4
var expectedAmount = request.Price * request.Quantity;
// #5
var orderTypeRepository = fixture.Freeze<Mock<IOrderTypeRepository>>();
orderTypeRepository.Setup(m => m.EnrichOrderTypeFieldAsync(It.IsAny<Order>()))
.ReturnsAsync((Order o) => o);
var orderRepository = fixture.Freeze<Mock<IOrderRepository>>();
orderRepository.Setup(m => m.CreateOrderAsync(It.IsAny<Order>()))
.ReturnsAsync((Order o) => o);
// #6
var underTest = fixture.Freeze<CreateOrderService>();
// Act
var actualResult = await underTest.CreateOrderAsync(request);
// Assert
orderRepository.Verify(m => m.CreateOrderAsync(It.IsAny<Order>()),
Times.AtLeastOnce());
orderTypeRepository.Verify(m => m.EnrichOrderTypeFieldAsync(It.IsAny<Order>()),
Times.AtLeastOnce());
actualResult.Amount.ShouldBe(expectedAmount);
}
In the test, the behavior we are testing is a call to the CreateOrderService
’s CreateOrderAsync
method. It just so happens that the OrderRepository
has a method similarly named. We are following the Arrange/Act/Assert pattern to define our test.
Let’s walk through the test.
Arrange
First, we setup the test.
- We create a fixture using the
AutoFixture
library. The library allows us to create all the objects needed for our test, as well as the SUT. Think of it as an object factory. - We tell the
Fixture
to use the Moq pluginAutoMoqCustomization
in order to create mocks. Otherwise, we would call theMoq
library directly. By doing this step, we delegate all mock creation to the fixture. - We use the fixture to build a request object, telling it to omit populating the ID field. The fixture will create a new object with all other properties populated with random data.
- After creating the request object, we expect what the calculated amount will be. We will use this to assert at the end of the test.
- Now we setup the mocks for the
CreateOrderService
’s dependencies. By using theFreeze
method, we ask the fixture to give us singleton instances of the mocks. In this way, any further use of typesIOrderTypeRepository
andIOrderRepository
will use the same instance internally. For each mock, we describe what the behavior of the calling methods should be. Looking at the example code, we call two methods on two dependencies:EnrichOrderTypeFieldAsync
andCreateOrderAsync
. The behavior is to simply return the same order object argument that is passed in. - Finally, we ask the fixture to give us an instance of our SUT, which is of type
CreateOrderService
. Under the hood, it will inject all required dependencies, in this case being the mocked singleton instances ofIOrderTypeRepository
andIOrderRepository
.
Act
Here, we call the method we are attempting to test: CreateOrderAsync
. The method does several things:
- It will validate the input
Order
object. The fixture will inject anOrderValidator
that will actually validate according to set rules. - It will calculate the amount and set it to the
Order
object’sAmount
field. - It will call
IOrderTypeRepository
’sEnrichOrderTypeFieldAsync
. We mocked this to just return the passed in argument. - It will call
IOrderRepository
’sCreateOrderAsync
. Again, we mocked this to just return the passed in argument.
Assert
Finally we make three assertions, using Shouldly’s awesome extension methods:
- We verify that
IOrderTypeRepository
’sEnrichOrderTypeFieldAsync
method was called at least once. This ensures that theOrder
object’sOrderType
field is being set. - We verify that
IOrderRepository
’sCreateOrderAsync
method was called at least once. This ensures that the order will be persisted to the data source. - Finally, we verify that the amount calculation is correct.
The first two assertions rely on the mocked objects acting as spies. The last assertion is directly comparing the actual amount value on the order to the expected value.
A Parameterized Test
If you need to test your code against a set of data, then you can use a parameterized test. In such a test, you define inputs to your test, and a source of test data. Here we can take advantage of xUnit
’s functionality.
The following example shows a test against an OrderValidator
class. In each execution of the test, we define an Order
object as input, but with a different property set as null
. We expect the validator to fail due to the null properties.
The test will run a total of 4 times, once for each Order
object created in the GetOrders
function.
[Theory]
[MemberData(nameof(GetOrders))]
public void ShouldFailWhenPropertyIsNull(Order order)
{
// Arrange
var fixture = new Fixture();
fixture.Inject(order);
var request = fixture.Freeze<Order>();
var underTest = fixture.Freeze<OrderValidator>();
// Act
var actual = underTest.Validate(request);
// Assert
actual.IsValid.ShouldBeFalse();
}
public static IEnumerable<object[]> GetOrders()
{
var fixture = new Fixture();
yield return new object[] { fixture.Build<Order>().Without(o => o.OrderId).Create() };
yield return new object[] { fixture.Build<Order>().Without(o => o.Price).Create() };
yield return new object[] { fixture.Build<Order>().Without(o => o.Quantity).Create() };
yield return new object[] { fixture.Build<Order>().Without(o => o.Symbol).Create() };
}
Conclusion
In this article, we covered a lot of terminology, concepts, and example code. With these practicals in hand, I hope this opens the door to a better unit testing experience for you!
Jose Chavez
Jose is a software engineer that cares about increasing the visiblity of underestimated individuals in technology.