In my previous post I mentioned that large unit test setups can be difficult to maintain / understand. This problem can be reduced by following a test pattern. I want to share with you a pattern I use, and I think works really well. In this post I will be using C#, Moq libary and Visual Studio test tools.

To start here are some things I believe make a good suite of unit tests:

Easy to identify data dependencies of tests.

Mocking is not repeated every test.

Test are clear and easy to understand / maintain.

New tests can be added with relative ease.

In order to explain this testing pattern I have forked my buddies TodoMVC app, and introduced a service layer, repository pattern and test project. You can see my source code with an example test class on github.

The TodoModule (service layer) has a number of public methods; Get, Add, Remove, etc. we will be testing the Get and Remove. The basic rule of the patterns is, “setup your mocks in one place, and allow each test to alter the data returned“. Below is a screenshot of the test class; you can also view a text version.

namespace NavigationTodoMVC.Tests.Service { using System.Collections.Generic; using System.Linq; using Data; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; using NavigationTodoMVC.Service; [TestClass] public class TodoModuleTests { private TodoModule _todoModule; private Mock<ITodoRepository> _todoRepository; private List<Todo> _todos; private List<Todo> _todosAdded; private List<Todo> _todosRemoved; [TestInitialize] public void Setup() { _todoRepository = new Mock<ITodoRepository>(); _todos = new List<Todo>(); _todosAdded = new List<Todo>(); _todosRemoved = new List<Todo>(); _todoRepository.Setup(m => m.Todos).Returns(_todos.AsQueryable()); _todoRepository.Setup(m => m.Add(It.IsAny<Todo>())).Callback<Todo>(c => _todosAdded.Add(c)); _todoRepository.Setup(m => m.Remove(It.IsAny<Todo>())).Callback<Todo>(c => _todosRemoved.Add(c)); _todoModule = new TodoModule(_todoRepository.Object); } } }

Tests I have written in the past have been completely self contained, meaning that the class under test and all mocks are setup for each and every test. The approach I have taken here is to setup the class and mocks inside the test initialize, this means that we can set it up once and use across all tests. Note the creation of three lists: _todos, _todosAdded and _todosRemoved, these are created empty and passed to the mocking framework. This allows us to later add or assert against these in each unit test. The test below does exactly that.

[TestMethod] public void Get_ManyTodosExistAndAllRequested_AllTodoDetailsReturned() { // Arrange _todos.Add(new Todo { Title = "Todo 1" }); _todos.Add(new Todo { Title = "Todo 2" }); _todos.Add(new Todo { Title = "Todo 3" }); // Act var actual = _todoModule.Get(StatusEnum.All); // Assert Assert.AreEqual(3, actual.Todos.Count()); }

Mocking in a central place has allowed us to see clearly the purpose and function of the test, whilst not hiding away any of the important data setup that helps you understand it. This also works when asserting that a Todo has been removed from the system. In this case we can assert against the pre-mocked list of todos removed.

[TestMethod] public void Remove_TodoExists_TodoRemoved() { // Arrange const int id = 56; _todos.Add(new Todo{Id = id, Title = "Todo Item 1"}); // Act _todoModule.Remove(id); // Assert Assert.AreEqual(1, _todosRemoved.Count()); Assert.AreEqual(id, _todosRemoved.First().Id); }

This pattern might seem obvious, but if you stick to it your test will be much easier to maintain, extend and understand. Lastly I want to call out a couple of things you should NOT be tempted to do:

Add data setup in the test initialize – This will mean that you cannot only look at a test to understand its purpose, you also need to check what data has been created when initialized.

Create ‘Helper’ methods – It can be tempting to create a helper method when setting up the same data multiple times, but for the same reasons above this will only make your test harder to understand.