Welcome to today’s post.
I will discuss how to use the third-party utility Moq to unit test using .NET Core.
In a previous post, I showed how to setup a test suite for unit testing with NUnit. In this post I will be showing how to apply unit testing with the popular third-party Moq library.
Moq is a popular open-source unit testing library that is used for helping create TDD based mock classes and method stubs that can be used within unit tests for .NET applications. It relies on the principle that we do not need to construct real implementations of objects and classes to test them. By constructing tests that target key business rules early in the development lifecycle, we can build a suite of tests that can be used in an automated continuous integration build to catch bugs that are introduced into the code base following source commits and check-ins.
Why write Mock Unit Tests?
The beauty with using a mocking framework for unit testing your application is that you can start before you have written any code. That is after all the principle behind the test-driven-development (TDD) approach. Once you have an idea of the requirements and data needs, then you can craft mock tests for the business logic. This can include writing tests that cover expected outputs of methods and functions. They can also include testing the status of data following data manipulation methods.
In many of the above types of tests, we do not even have to implement the test code within the test method to return a result, as the test implementation can be mocked! We could start returning default results for each test function until we have completed building the internal test logic. The methods in this case are stubs. The same applies for data outputs such as lists, as we can even mock that out by returning defaults to start with.
Installing Moq
To install Moq, search for it using the NuGet package manager then install it into your unit test project.
Using Moq in your Unit Test
To use Moq in a unit test simply use the following declaration near the top of the source file:
using Moq;
Defining the Interfaces
We will next need to define interfaces that are used to define properties and operations for our classes.
A basic set of interfaces is shown below:
using System;
using System.Collections.Generic;
using System.Text;
using BookLoan.Data;
using BookLoan.Models;
namespace BookLoanTest.TDD.Moq
{
public interface MockApplicationDbContext
{
int Add(object record);
int Delete(object record);
int SaveChanges();
}
public interface MockBookService
{
int bookCount { get; set; }
List<BookViewModel> GetBooks();
List<BookViewModel> GetBooksFilter(string filter);
}
}
How Interfaces are used in a typical Unit Test
The interfaces will then be used to create mock instances of our classes that are then setup to be used to implement test cases within our unit test methods.
An example unit test that implements a test case where three records are added to the book table and the record count is confirmed with an assertion.
[Test]
public void ListBooksTest()
{
var book = new BookViewModel() {
ID = 1, Author = "W.Smith",
Title = "Rivers Run Dry", YearPublished = 1954
};
var mockDataContext = new Mock<MockApplicationDbContext>();
var mockBookService = new Mock<MockBookService>();
// setup properties
mockBookService.SetupAllProperties();
mockBookService.SetupProperty(f => f.bookCount, 0);
// setup first record insertion
mockDataContext.Setup(p => p.Add(book))
.Callback(() => mockBookService.Object.bookCount++);
mockDataContext.Setup(p => p.SaveChanges());
// action insertion and save record
mockDataContext.Object.Add(book);
mockDataContext.Object.SaveChanges();
book = new BookViewModel() {
ID = 2, Author = "D.Lee", Title = "Red Mine",
YearPublished = 1968
};
// setup second record insertion
mockDataContext.Setup(p => p.Add(book))
.Callback(() => mockBookService.Object.bookCount++);
mockDataContext.Setup(p => p.SaveChanges());
// action insertion and save record
mockDataContext.Object.Add(book);
mockDataContext.Object.SaveChanges();
book = new BookViewModel() {
ID = 3, Author = "V.Prescott", Title = "Boundary Rider",
YearPublished = 1974
};
// setup third record insertio
mockDataContext.Setup(p => p.Add(book))
.Callback(() => mockBookService.Object.bookCount++);
mockDataContext.Setup(p => p.SaveChanges());
// action insertion and save record
mockDataContext.Object.Add(book);
mockDataContext.Object.SaveChanges();
// retrieve record count property
var bookCount = mockBookService.Object.bookCount;
// test result
Assert.AreEqual(3, bookCount);
}
Setup stage of tests
In the first two lines:
var mockDataContext = new Mock<MockApplicationDbContext>();
var mockBookService = new Mock<MockBookService>();
we are setting up the mocking classes for the data context and book service. At this stage of development, we have yet to determine the implementation of the classes and can only provide the interface contracts that contain methods and properties (accessors).
This is the power of Moq and other mocking libraries out there: we are not required to create instances of the classes and we don’t need to use additional libraries like dependency injection or in-memory data providers in order to simulate a mock and an expected result. This allows us to build powerful unit tests with minimal resourcing in an agile development environment.
With the next two lines:
mockBookService.SetupAllProperties();
mockBookService.SetupProperty(f => f.bookCount, 0);
the purpose is to setup all the properties within the mock class as accessors (getters and setters).
The second line setup up the bookCount property with a default value of zero.
With the next two lines:
mockDataContext.Setup(p => p.Add(book))
.Callback(() => mockBookService.Object.bookCount++);
mockDataContext.Setup(p => p.SaveChanges());
the purpose is to setup the SaveChanges() and Add() methods. With the Add() method it will accept a book object, then run a call back which increases the bookCount property.
Setup actions of tests
The next two lines then apply the actions of adding a new book, then saving changes to the data context:
mockDataContext.Object.Add(book);
mockDataContext.Object.SaveChanges();
The above setups and actions are then repeated a two further two times before we verify the record count with an assertion:
var bookCount = mockBookService.Object.bookCount;
Assert.AreEqual(3, bookCount);
The above shows how to perform state based TDD unit testing.
We can amend the above to simulate behavior driven testing and behavior driven development (or BDD) testing.
Testing Behavior Sequences
A unit test that enforces adding and deletion of records in the correct order is shown:
[Test]
public void AddAndDeleteBookTest()
{
var book = new BookViewModel() {
ID = 1, Author = "W.Smith",
Title = "Rivers Run Dry", YearPublished = 1954
};
var mockDataContext = new
Mock<MockApplicationDbContext>(MockBehavior.Strict);
var mockBookService = new Mock<MockBookService>();
var sequence = new MockSequence();
mockDataContext.InSequence(sequence).Setup(x =>
x.Add(book)).Callback(
() => mockBookService.Object.bookCount++).Returns(1);
mockDataContext.InSequence(sequence).Setup(x =>
x.Delete(book)).Callback(
() => mockBookService.Object.bookCount--).Returns(1);
mockDataContext.InSequence(sequence).Setup(x =>
x.SaveChanges()).Returns(1);
mockDataContext.Object.Add(book);
mockDataContext.Object.Delete(book);
mockDataContext.Object.SaveChanges();
mockDataContext.VerifyAll();
var bookCount = mockBookService.Object.bookCount;
Assert.AreEqual(0, bookCount);
}
To enforce expectations on actions, we declare our mock instance as shown:
var mockDataContext = new Mock<MockApplicationDbContext>(MockBehavior,Strict);
This behavior test shows us enforcing an order on actions.
Below is the setup to enforce a test on adding and removing records from a database. This is done with an expectation setup using a mock sequence as follows:
var mockBookService = new Mock<MockBookService>();
var sequence = new MockSequence();
mockDataContext.InSequence(sequence).Setup(x =>
x.Add(book)).Callback(
() => mockBookService.Object.bookCount++).Returns(1);
mockDataContext.InSequence(sequence).Setup(x =>
x.Delete(book)).Callback(
() => mockBookService.Object.bookCount--).Returns(1);
mockDataContext.InSequence(sequence).Setup(x =>
x.SaveChanges()).Returns(1);
The actions are then called in the following order:
mockDataContext.Object.Add(book);
mockDataContext.Object.Delete(book);
mockDataContext.Object.SaveChanges();
The action order is then verified against the expected order:
mockDataContext.VerifyAll();
When run, the above actions should pass the sequencing expectation and the final assertion.
To test an order that fails expectation we can setup the actions or expectations in a different order. In this case I will run the actions in a different ordering than the actions in the setup sequence.
When our expected actions are setup in the following ordering:
mockDataContext.InSequence(sequence).Setup(x =>
x.Add(book)).Callback(
() => mockBookService.Object.bookCount++).Returns(1);
mockDataContext.InSequence(sequence).Setup(x =>
x.Delete(book)).Callback(
() => mockBookService.Object.bookCount--).Returns(1);
mockDataContext.InSequence(sequence).Setup(x =>
x.SaveChanges()).Returns(1);
With Add() followed by Delete() followed by SaveChanges().
If we attempt to call the actions in a different order, we should expect an exception to be thrown.
try
{
mockDataContext.Object.Add(book);
mockDataContext.Object.SaveChanges();
mockDataContext.Object.Delete(book);
Assert.Fail();
}
catch (MockException ex)
{
Assert.Pass();
}
There is no need to use VerifyAll() as the call to SaveChanges() will throw a mock exception.
Without the catch the following will be thrown in the Test Explorer:
The above overview of Moq has shown how to use it to implement some useful unit tests that can be used in many of scenarios.
For additional information and examples see:
https://github.com/devlooped/moq
https://github.com/devlooped/moq/wiki/Quickstart
That’s all for today’s post.
I hope you found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.