Testing In .Net with xUnit & Moq
Today I want to cover some simple testing setup and configuration. I'll be using my example project from the Dapper & CQRS post I made last week so consider this post a part 2. For tooling I'll be using xUnit and Moq as well as a few other nuget packages to easily configure and run tests in Visual Studio. I am working on a Mac, but operating system shouldn't matter.
We all know we should be testing our code. The company I work for has been woefully bad at it for years, but we have been working lately at developing a testing strategy and we're making great strides in this area. Moq and xUnit make testing pretty easy and allow us to catch some bugs early. Dare I even say, testing could possibly even become enjoyable...?
Ok so maybe fun is a stretch, but it's still important. So let's get started shall we!
Code
You can get all the source code for this example at https://github.com/MelodicDevelopment/example-dotnet-api-cqrs.
Tests
The dotnet-api-cqrs.tests project contains (simplistic) examples for doing integration as well as unit testing. This is pretty geared towards our particular Dapper / CQRS pattern, however it could be useful for other patterns as well.
One thing to take note is that we configured our tests to run against an actual database. You can mock data if you'd like, but we chose to run our queries and commands against our CI database. This gives us the added bonus of testing to ensure database changes have not occurred that would break our code. The queries will run as expected against the database, however commands are all rolled back so that nothing is actually committed to the database. To achieve this, we've created a TestDbContext which is based on our DbContext from the dotnet-api-cqrs.data project. The TestDbContext extends and overwrites most the virtual methods on the DbContext class. This way we can differentiate between queries and commands and for commands we have an addition setting to rollback comamnds. This will attempt to execute the command, but not actually commit any changes to the database. This way we get to see if any errors happen in the command without actually affecting the database. We can catch whatever exceptions the database throws, such as duplicate key errors, or unique key exceptions.
dotnet-api-cqrs.tests/TestDbContext.cs
using System;
using System.Collections.Generic;
using System.Data;
using dotnet_api_cqrs.data;
namespace dotnet_api_cqrs.tests
{
public class TestDbContext : DbContext
{
private readonly bool _noCommit;
private bool _isQuery;
public TestDbContext(string connectionString, bool noCommit = false) : base(connectionString)
{
_noCommit = noCommit;
}
public override IEnumerable<T> Query<T>(string query, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null)
{
_isQuery = true;
return base.Query<T>(query, param, commandType, transaction);
}
public override T QueryFirst<T>(string query, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null)
{
_isQuery = true;
return base.QueryFirst<T>(query, param, commandType, transaction);
}
public override int InsertSingle(string sql, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null, int? timeout = null)
{
_isQuery = false;
return base.InsertSingle(sql, param, commandType, transaction, timeout);
}
public override int Command(string sql, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null, int? timeout = null)
{
_isQuery = false;
return base.Command(sql, param, commandType, transaction, timeout);
}
public override T Transaction<T>(Func<IDbTransaction, T> query)
{
if (!_noCommit || _isQuery) {
_isQuery = false;
return base.Transaction(query);
} else {
_isQuery = false;
using var connection = Connection;
using var transaction = BeginTransaction();
try {
var result = query(transaction);
} catch (Exception) {
throw;
}
transaction.Rollback();
return default;
}
}
public override void Transaction(Action<IDbTransaction> query)
{
if (!_noCommit || _isQuery) {
_isQuery = false;
base.Transaction(query);
} else {
_isQuery = false;
using var connection = Connection;
using var transaction = BeginTransaction();
try {
query(transaction);
} catch (Exception) {
throw;
}
transaction.Rollback();
}
}
~TestDbContext()
{
Dispose();
}
}
}
So our tests are against a real live database... but not our production database. We're testing it live, but not really live... and we don't commit. So... it all works out right? Seems to be working for us.
Required Nuget Packages
If you view the nuget package manager in Visual Studio you can see which packages are required for this, but in short they are:
- Moq
- xUnit
- xUnit.Runner.VisualStudio
- Microsoft.AspNetCore.Mvc.Testing
- Microsoft.AspNetCore.TestHost
- Microsoft.NET.Test.Sdk
Unit Tests
For unit tests we'll look at how we tested on of our queries and one of our commands. To make sure we were using the correct TestDbContext I created a base class that all our unit tests extend. It's called TestBase. It news up a TestDbContext and allows for passing in a noCommit flag. This flag prevents commands from being committed to the database.
dotnet-api-cqrs.tests/TestBase.cs
using dotnet_api_cqrs.contracts.data;
namespace dotnet_api_cqrs.tests
{
public abstract class TestBase
{
private readonly bool _noCommit;
protected readonly IDbContext TestDbContext;
public TestBase(bool noCommit = false)
{
_noCommit = noCommit;
TestDbContext = new TestDbContext($"use-configuration-to-get-connection-string", _noCommit);
}
}
}
dotnet-api-cqrs.tests/Data/Book/BookQueryTests.cs
using dotnet_api_cqrs.data.Queries.Book;
using Xunit;
namespace dotnet_api_cqrs.tests.Data.Book
{
public class BookQueryTests : TestBase
{
[Fact]
public void GetAllBooksTest()
{
var query = new GetAllBooksQuery();
var results = query.Execute(TestDbContext);
Assert.NotEmpty(results);
}
[Theory]
[InlineData(1)]
public void GetBookByIDTest(int bookID)
{
var query = new GetBookQuery(bookID);
var results = query.Execute(TestDbContext);
Assert.NotNull(results);
}
[Theory]
[InlineData(1)]
public void GetBookForAuthorTest(int authorID)
{
var query = new GetBooksForAuthorQuery(authorID);
var results = query.Execute(TestDbContext);
Assert.NotNull(results);
}
}
}
In the BookQueryTests.cs file you will see multiple tests that can be run. These are 2 simplistic tests that will test all three of our queries you can find in the dotnet-api-cqrs.data project. xUnit allows for a couple method attributes. The first one is Fact
and this one simply runs the test with no parameters. The Theory
attribute allows for one more attribute called InlineData
. In the InlineData
attribute you can pass arguments which will then be passed to the test method. You can see this working in the GetBookByIDTest
test method. Again, this is a very simplistic test and we just pass a 1 which indicates the book ID to search for. These tests can be written to be much more complex and test various scenarios with much more complicated test data. We won't get into that here for sake of time, but go check out their documentation and do some google searching. There's lots of good stuff out there to help write more complex and stringent unit tests.
Using Moq To Test A Service
Moq allows you to very easily setup a mock object that can be used inside your unit tests. For another simple and basic example we have our BookServiceTests.cs file. This again extends the TestBase class and uses Moq to create a mock object of the IBookFacade which the BookService is expecting as a constructor argument. In this example, we create a mock of IBookFacade, and configure it so that when the GetBooks method is called we pass back some test data. This way when the BookService calls the GetBooks method on the facade it will get some test data and perform whatever business logic we have. Then we can test the result.
dotnet-api-cqrs.tests/Services/BookServicesTests.cs
using dotnet_api_cqrs.contracts.data;
using dotnet_api_cqrs.data;
using dotnet_api_cqrs.services;
using Moq;
using Xunit;
namespace dotnet_api_cqrs.tests.Services
{
public class BookServicesTests : TestBase
{
[Fact]
public void GetAllBooksTest()
{
var mockFacade = new Mock<IBookFacade>();
mockFacade.Setup(repo => repo.GetBooks())
.Returns((context, transaction) => {
return TestData.Books;
});
var bookService = new BookService(TestDbContext, mockFacade.Object);
Assert.NotEmpty(bookService.GetAllBooks());
}
}
}
Integration Tests
We can also use xUnit and the Microsoft.AspNetCore.Mvc.Testing nuget package to run integration tests. These integration tests will setup a virtual web server in memory, run the api project on that virtual server, and then hit the endpoints to test them from the api endpoint down to the database query or command and return any issues along the way.
The first piece we need is called the WebApplicationFactory. The Mvc.Testing package supplies this base class and we just extend it a little bit so that we can overwrite our default IDbContext dependency injection with the TestDbContext.
dotnet-api-cqrs.tests/api/ApiQueryTestApplicationFactory.cs
using dotnet_api_cqrs.contracts.data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace dotnet_api_cqrs.tests.Api
{
public class ApiQueryTestApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureTestServices(services => {
services.RemoveAll<IDbContext>();
services.TryAddScoped<IDbContext>(sp => new TestDbContext($"use-configuration-to-get-connection-string"));
});
}
}
}
This particular factory class is for testing queries. We have a separate one that will test when commands are being run. These are rather simplistic tests I realize and we're still working on a solid strategry for testing more complex business logic that will run both commands and queries in one pass, but this should get you started anyway.
Now if you look at the BookControllerQueryTests file you can see one more test where we hit the end point on the BookController that returns all books. In this particular test we are simply testing that the call was made succesfully.
dotnet-api-cqrs.tests/Api/Controllers/BookControllerQueryTests.cs
using System.Net.Http;
using System.Threading.Tasks;
using dotnet_api_cqrs.api;
using Xunit;
namespace dotnet_api_cqrs.tests.Api.Controllers
{
public class BookControllerTests : IClassFixture<ApiQueryTestApplicationFactory<Startup>>
{
private readonly ApiQueryTestApplicationFactory<Startup> _factory;
private readonly HttpClient _httpClient;
public BookControllerTests(ApiQueryTestApplicationFactory<Startup> factory)
{
_factory = factory;
_httpClient = _factory.CreateClient();
}
[Fact]
public async Task GetBooks_IsSuccessful()
{
var response = await _httpClient.GetAsync($"/api/book");
response.EnsureSuccessStatusCode();
Assert.True(response.IsSuccessStatusCode);
}
}
}
Conclusion
So again, this was a rather simplistic look at unit and integration testing using xUnit and Moq, but I hope it gets you started on the road to more thorough and complex testing in your own code to ensure you are creating the best products you can.
Please let me know if the comments your thoughts.
Top comments (2)
Nice! I will try to follow your guide, I hope everything is fine because I had problems with the latest version of XUnit, vs simply did not recognize the tests
I believe this nuget package is what gets visual studio to recognize the tests in the test runner.
xUnit.Runner.VisualStudio