Unit Testing is extremely important for creating robust software. It’s very simple in principle but it can get a little bit tricky when it comes to real-world applications that depend on databases. In this article, we’re going to explore some approaches to testing repository pattern in our ASP.NET Core applications, and we’re going to implement one of them.
Let’s start.
Unit Testing and Test Doubles
Unit Testing is the process of writing code to test our code. As the name implies, it treats each part of the application (usually methods) as a unit. In order for a method to be a testable unit, it has to be isolated; meaning it doesn’t have any external dependencies. However, in real-world applications, our methods usually depend on external dependencies, like databases or APIs. In order to tackle that, we need to replace these dependencies with fake objects or doubles, through which we can simulate these external processes.
Test doubles have many types, the most used ones are Fakes, Stubs, and Mocks. Let’s go over each one.
Fakes are static replacements for external dependencies. For example, if we’re calling some API on some server, we can create a similar API on a local server and call it instead.
Stubs are fake objects where we override the actual implementation with a predefined output. For example, we can configure methods that query the database to return a constant value.
Mocks are more dynamic than the previous two. In mocks we override the actual implementation with another implementation, so for example, instead of querying the database, we can provide some other logic that queries a static data structure.
Now that we have explained some unit testing basics, let’s explore how we can use them in real-world applications.
Testing Without the Database
Reading and writing from the database are part of almost every method in real-world applications. When we create unit tests for these methods, we want to use a double for the database. We definitely don’t want to read and write the production database.
So, let’s discover some alternatives that we can use as a data source.
Using in-memory SQLite
SQLite is a lightweight database engine. One of the main purposes of SQLite is to substitute databases during tests or demos. SQLite can be configured to be in memory, instead of a hard disk. In this case, a new database will be created when we open a connection with the database engine. We can use this fake database to substitute our database calls. When we’re done, we close the connection, and the database will be deleted.
Although this achieves a good deal of isolation, since each method is basically dealing with its own database, it is not a favorable option. This is because we’re most probably using a different database engine than the one our code calls. This might lead to some inconsistent results, making our tests inaccurate. Also, this is not unit testing, because we’re relying on an external dependency, even though it’s in-memory.
Using EF Core in-memory provider
Similar to SQLite, Entity Framework Core has an in-memory provider. This was originally designed to test EF Core itself, but it’s still can be used to substitute databases. However, this is wouldn’t be our go-to option either. EF Core in-memory provider has the same disadvantages as SQLite. Furthermore, it’s not even a database engine, so we wouldn’t be able to run raw SQL against it, or use transactions.
This is also not considered unit testing, so we won’t be covering it in detail.
Mocking DbContext
Another approach to creating a test double for the database is by mocking DbContext
and DbSet
. These mock objects should have their own implementation, in which we can avoid calling the actual database. However, this is only doable for non-querying functionalities. We can easily override AddAsync()
and SaveChangesAsync()
methods, but querying against DbSets is done using LINQ, which are static methods, and therefore cannot be mocked.
Introducing Repository Layer and Mocking It
The Repository Pattern has been very popular in the last decade. Almost every Domain-Driven Designed application implements it. It’s basically a layer that holds logic responsible for accessing data sources; databases mainly. Having implemented it, we don’t have to call the database directly from our core logic. Instead of depending on an external data source, we call the repository, which is a logic layer and hence very easy to mock.
This is our favorite approach to creating unit tests for our applications, and we’re going to implement an example in a minute. We strongly recommend reading our article about Repository Pattern, it is recommended to check it out if you’re not quite familiar with the pattern.
Creating Unit Tests For a Real-World Application
We are going to create unit tests for an already existing project, you can check out the source code here. We strongly recommend downloading and going over it before proceeding with this article.
Adding the xUnit Testing Project
Let’s start by creating an xUnit testing project and calling it AccountOwnerServer.Tests
. You can use the Visual Studio template or run the command dotnet new xunit –name AccountOwnerServer.Tests
.
The created project should have xUnit and xUnit runner installed out of the box. It also has a test class named UnitTest1.cs
. Let’s delete it and create a new test class and name it OwnerControllerTests.cs
.
We will be using the Moq library for mocking, so let’s install and we’re ready to go. We can do that using NuGet Package Manager, or by running the following command in PowerShell: Install-Package Moq
. Let’s also add a project reference of AccountOwnerServer
to our testing project, and we’re good to go.
In this example, we have one controller, OwnerController
. We’re going to create unit tests for its action methods. So, let’s first go over our controller dependencies:
public class OwnerController : ControllerBase { private ILoggerManager _logger; private IRepositoryWrapper _repository; private IMapper _mapper; public OwnerController(ILoggerManager logger, IRepositoryWrapper repository, IMapper mapper) { _logger = logger; _repository = repository; _mapper = mapper; } ... }
We notice that we have three dependencies: IloggerManager
, IRepositoryWrapper
, and IMapper
. When we go over the code we find out that the logger and the mapper don’t interact with any external dependencies, so we can use them in our tests without violating the isolation of our tests. However, the _repositoryWrapper
calls the database, and therefore we need to mock it.
Mocking IRepositoryWrapper
Let’s create a folder “Mocks” in our testing project. This folder will contain static classes responsible for configuring the mocks that we need in our tests. We create a class named MockIRepositoryWrapper
:
internal class MockRepositoryWrapper { public static Mock<IRepositoryWrapper> GetMock() { var mock = new Mock<IRepositoryWrapper>(); // Setup the mock return mock; } }
This static class has one static method GetMock()
that’s responsible for creating the IRepositoryWrapper
mock, setting it up (we will), and returning the mock. Now, we need to set up the mock by overriding the actual behavior with our mock behavior. The IRepositortWrapper
has two properties of types IOwnerRepository
and IAccountRepository
, and one method Save()
.
Let’s first override the properties so that they return mocks:
public static Mock<IRepositoryWrapper> GetMock() { var mock = new Mock<IRepositoryWrapper>(); mock.Setup(m => m.Owner).Returns(() => new Mock<IOwnerRepository>().Object); mock.Setup(m => m.Account).Returns(() => new Mock<IAccountRepository>().Object); return mock; }
Here we configure each property to return a mock object. So for example, if we’re testing a method that uses the Owner
property, the execution will use this mock object that we just provided. We are currently returning plain mocks, but we actually need to set up these mocks too, we’ll do that in a minute. Let’s now set up the Save()
method:
public static Mock<IRepositoryWrapper> GetMock() { var mock = new Mock<IRepositoryWrapper>(); mock.Setup(m => m.Owner).Returns(() => new Mock<IOwnerRepository>().Object); mock.Setup(m => m.Account).Returns(() => new Mock<IAccountRepository>().Object); mock.Setup(m => m.Save()).Callback(() => { return; }); return mock; }
This method is only responsible for writing to the database, which is outside of our testing scope, so we just want to skip it. So here we’re overriding it with an Action
that just returns void
.
This mock is not quite ready yet, we have to properly mock IOwnerRepository
and IAccountRepository
and use them in this mock.
Mocking IOwnerRepository
Let’s create a new class in the “Mocks
” folder, named MockIOwnerRepository.cs
. Similarly, it contains one static method that sets up a mock and returns it:
internal class MockIOwnerRepository { public static Mock<IOwnerRepository> GetMock() { var mock = new Mock<IOwnerRepository>(); var owners = new List<Owner>() { new Owner() { Id = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e"), Name = "John", DateOfBirth = DateTime.Now.AddYears(-20), Accounts = new List<Account>() { new Account() { Id = new Guid(), AccountType = "", DateCreated = DateTime.Now } } } }; // Set up return mock; } }
This mock has one addition. The owners
variable, which is just a fake list of owners. We’re going to use this list as our database table double.
Now, IOwnerRepository
has a bunch of methods, and we need to set up each one of them, we’ll do them one by one:
mock.Setup(m => m.GetAllOwners()) .Returns(() => owners);
Here, we’re overriding GetAllOwners()
method, so that when we test a method that calls it, our fake list returns instead of actual data from the database:
mock.Setup(m => m.GetOwnerById(It.IsAny<Guid>())) .Returns((Guid id) => owners.FirstOrDefault(o => o.Id == id));
We’re overriding GetOwnerById()
, but this method takes up a parameter of type Guid
. Here’s when things become more complicated, so let’s explain this line in detail.
The It.IsAny<Guid>()
method matches the argument type of the method, which basically means that when this method is called with any argument of type Guid
, the following Func
will run instead. This is the Func
that we passed to the Returns()
method, it takes an id as a parameter and returns the first occurrence that matches this id from owners
, or null:
mock.Setup(m => m.GetOwnerWithDetails(It.IsAny<Guid>())) .Returns((Guid id) => owners.FirstOrDefault(o => o.Id == id));
Similarly, we set up the GetOwnerWithDetails()
method.
Now we’re done with the methods that read from the database. For the methods writing to the database we don’t actually need to provide them with any logic, just skip them as we’ve done with the Save()
method:
mock.Setup(m => m.CreateOwner(It.IsAny<Owner>())) .Callback(() => { return; }); mock.Setup(m => m.UpdateOwner(It.IsAny<Owner>())) .Callback(() => { return; }); mock.Setup(m => m.DeleteOwner(It.IsAny<Owner>())) .Callback(() => { return; });
Now, our IOwnerRepository
mock is ready. The final class should look like this:
internal class MockIOwnerRepository { public static Mock<IOwnerRepository> GetMock() { var mock = new Mock<IOwnerRepository>(); var owners = new List<Owner>() { new Owner() { Id = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e"), Name = "John", DateOfBirth = DateTime.Now.AddYears(-20), Accounts = new List<Account>() { new Account() { Id = new Guid(), AccountType = "", DateCreated = DateTime.Now } } } }; mock.Setup(m => m.GetAllOwners()).Returns(() => owners); mock.Setup(m => m.GetOwnerById(It.IsAny<Guid>())) .Returns((Guid id) => owners.FirstOrDefault(o => o.Id == id)); mock.Setup(m => m.GetOwnerWithDetails(It.IsAny<Guid>())) .Returns((Guid id) => owners.FirstOrDefault(o => o.Id == id)); mock.Setup(m => m.CreateOwner(It.IsAny<Owner>())) .Callback(() => { return; }); mock.Setup(m => m.UpdateOwner(It.IsAny<Owner>())) .Callback(() => { return; }); mock.Setup(m => m.DeleteOwner(It.IsAny<Owner>())) .Callback(() => { return; }); return mock; } }
Mocking IAccountRepository
Next, let’s do the same process with IAccountRepository
:
internal class MockIAccountRepository { public static Mock<IAccountRepository> GetMock() { var mock = new Mock<IAccountRepository>(); var accounts = new List<Account>() { new Account() { OwnerId = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e") } }; mock.Setup(m => m.AccountsByOwner(It.IsAny<Guid>())) .Returns((Guid id) => accounts.Where(a => a.OwnerId == id).ToList()); return mock; } }
We create a mock using Moq, then create a fake list of accounts, set up the AccountsByOwner()
method to search our fake list instead of the database, and finally return the mock.
That’s it, both our repositories are properly mocked. Let’s now use them in the MockIRepositoryWrapper
class:
public static Mock<IRepositoryWrapper> GetMock() { var mock = new Mock<IRepositoryWrapper>(); var ownerRepoMock = MockIOwnerRepository.GetMock(); var accountRepoMock = MockIAccountRepository.GetMock(); mock.Setup(m => m.Owner).Returns(() => ownerRepoMock.Object); mock.Setup(m => m.Account).Returns(() => accountRepoMock.Object); mock.Setup(m => m.Save()).Callback(() => { return; }); return mock; }
Creating Tests for GET Methods in AccountController
Now, we’re ready to write tests for the AccountController
. As we’ve mentioned, this controller has three dependencies, and we’ve already mocked one of them – the IRepostioryWrapper
. The ILogger
will just be replaced by a regular instance. However, we have to configure the mapper just the same way we did in the AccountServer
project:
public class OwnerControllerTests { public IMapper GetMapper() { var mappingProfile = new MappingProfile(); var configuration = new MapperConfiguration(cfg => cfg.AddProfile(mappingProfile)); return new Mapper(configuration); } }
This method just configures an instance that implements IMapper
and returns it. We’re going to use it in our tests. We’ve also added some using statements that we’re going to need in this test class.
Now, let’s create our first test for the GetAllOwners()
action method:
[Fact] public void WhenGettingAllOwners_ThenAllOwnersReturn() { var repositoryWrapperMock = MockIRepositoryWrapper.GetMock(); var mapper = GetMapper(); var logger = new LoggerManager(); var ownerController = new OwnerController(logger, repositoryWrapperMock.Object, mapper); var result = ownerController.GetAllOwners() as ObjectResult; Assert.NotNull(result); Assert.Equal(StatusCodes.Status200OK, result.StatusCode); Assert.IsAssignableFrom<IEnumerable<OwnerDto>>(result.Value); Assert.NotEmpty(result.Value as IEnumerable<OwnerDto>); }
The first thing we notice is how we name our test methods. Its name should have two parts, separated by an underscore. The first part describes the scenario we are testing, the second part is the expected behavior.
We then start to arrange, creating an object of the controller and passing its dependencies. Next, we act, by calling the method we’re testing, we’re casting the result to ObjectResult
so that we can assert the action result contents. Finally, we make some assertions.
Let’s run this test:
And the method passed the test. This method doesn’t have any other case to consider, so this test is just enough.
Now, let’s test the GetOwnerById()
method. This method has two cases: it either finds the owner and returns it, or doesn’t find them and returns 404
. We should create a test method for each case.
Let’s start with the former:
[Fact] public void GivenAnIdOfAnExistingOwner_WhenGettingOwnerById_ThenOwnerReturns() { var repositoryWrapperMock = MockIRepositoryWrapper.GetMock(); var mapper = GetMapper(); var logger = new LoggerManager(); var ownerController = new OwnerController(logger, repositoryWrapperMock.Object, mapper); var id = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e"); var result = ownerController.GetOwnerById(id) as ObjectResult; Assert.NotNull(result); Assert.Equal(StatusCodes.Status200OK, result.StatusCode); Assert.IsAssignableFrom<OwnerDto>(result.Value); Assert.NotNull(result.Value as OwnerDto); }
In this test method’s name, we added a new part, which is the ‘Given’ part, which defines the preconditions of the state under test. We’re calling the GetOwnerById()
method, passing an id that we know exists, because it exists in the fake list we created in the mock.
Now, let’s test the other case:
[Fact] public void GivenAnIdOfANonExistingOwner_WhenGettingOwnerById_ThenNotFoundReturns() { var repositoryWrapperMock = MockIRepositoryWrapper.GetMock(); var mapper = GetMapper(); var logger = new LoggerManager(); var ownerController = new OwnerController(logger, repositoryWrapperMock.Object, mapper); var id = Guid.Parse("f4f4e3bf-afa6-4399-87b5-a3fe17572c4d"); var result = ownerController.GetOwnerById(id) as StatusCodeResult; Assert.NotNull(result); Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); }
Here, we’re passing an id that we know doesn’t exist in our fake list, and we just assert that the status code returning is 404
. Let’s run these two tests:
Perfect! Everything executes as we’ve expected so far.
Creating Tests for POST Methods of AccountController
We’ve created tests for two GET methods, let’s now try to test a POST one. Let’s check out the CreateOwner()
method.
public IActionResult CreateOwner([FromBody] OwnerForCreationDto owner) { try { if (owner is null) { _logger.LogError("Owner object sent from client is null."); return BadRequest("Owner object is null"); } if (!ModelState.IsValid) { _logger.LogError("Invalid owner object sent from client."); return BadRequest("Invalid model object"); } var ownerEntity = _mapper.Map<Owner>(owner); _repository.Owner.CreateOwner(ownerEntity); _repository.Save(); var createdOwner = _mapper.Map<OwnerDto>(ownerEntity); return CreatedAtRoute("OwnerById", new { id = createdOwner.Id }, createdOwner); } catch (Exception ex) { _logger.LogError($"Something went wrong inside CreateOwner action: {ex.Message}"); return StatusCode(500, "Internal server error"); } }
As we can see, most of the logic done in this method is for validating the request object. In fact, this is the main thing our server is responsible for, ensuring that valid data is written to the database. So, we want to test the validation functionality, but since this is done by the framework, we have to mock it.
Mocking Model Validation
Let’s create another test class named ValidationTests
:
public class ValidationTests { private bool ValidateModel(object model) { var validationResults = new List<ValidationResult>(); var ctx = new ValidationContext(model, null, null); return Validator.TryValidateObject(model, ctx, validationResults, true); } }
We add some using directives that we’re going to need in this class. We also create a method that takes an object and validates it based on its data annotations. This is our mock of the real validation done by the framework. Now, we could have so many cases that we want to test, that our endpoint could receive a number of different invalid objects, so it doesn’t make sense that we write a test method for each case.
Instead, we’re going to use a new type of test method: Theory. A Theory is a method that we can provide with multiple inputs, and for each input, we provide an expected outcome. Eventually, when we run this test method, it runs for each input as a separate test case.
Let’s see how to do that using XUnit:
[Theory] [InlineData(null, null, null, false)] [InlineData(null, "TestAddress", null, false)] [InlineData(null, null, "06/04/1994", false)] [InlineData("TestName", null, null, false)] [InlineData(null, "TestAddress", "06/04/1994", false)] [InlineData("TestName", null, "06/04/1994", false)] [InlineData("TestName", "TestAddress", "06/04/1994", true)] [InlineData("TestTestTestTestTestTestTestTestTestTestTestTestTestTestTestTest", "TestAdress", "06/04/1994", false)] public void TestModelValidation(string? name, string? address, string? dateOfBirth, bool isValid) { var owner = new OwnerForCreationDto() { Address = address, Name = name, DateOfBirth = dateOfBirth is null ? DateTime.MinValue : DateTime.Parse(dateOfBirth) }; Assert.Equal(isValid, ValidateModel(owner)); }
A theory method is marked by the [Theory]
attribute. And we can provide it with inputs and expected outputs using the [InlineData]
attribute. We pass any number of arguments we need (they have to be constants), and the final argument is the expected outcome. In the method, we add matching parameters.
Inside the method, we create an object using the provided values, and we assert its validity:
So, although we only wrote one method, we actually created 9 tests. And they all passed, perfect!
Writing Positive Tests for Create Method
Now that we covered the validation part, let’s get back to OwnerControllerTests
class and create a successful test for the CreateOwner()
method:
[Fact] public void GivenValidRequest_WhenCreatingOwner_ThenCreatedReturns() { var repositoryWrapperMock = MockIRepositoryWrapper.GetMock(); var mapper = GetMapper(); var logger = new LoggerManager(); var ownerController = new OwnerController(logger, repositoryWrapperMock.Object, mapper); var owner = new OwnerForCreationDto() { Address = "TestAddress", Name = "TestName", DateOfBirth = new DateTime(1994, 7, 25) }; var result = ownerController.CreateOwner(owner) as ObjectResult; Assert.NotNull(result); Assert.IsAssignableFrom<CreatedAtRouteResult>(result); Assert.Equal((int)HttpStatusCode.Created, result!.StatusCode); Assert.Equal("OwnerById", (result as CreatedAtRouteResult)!.RouteName); }
In the above code, we pass a valid request which should succeed, and we make some assertions on the response.
Now, we don’t need to create any unsuccessful tests for this method, because all the unsuccessful scenarios are being tested by the ValidationTests
class.
We won’t go into testing PUT and DELETE methods, but you can try it out and let us know how it went.
Conclusion
In this article, we’ve discussed different ways of testing a repository pattern without including a database. We’ve implemented this approach on an example API project, and explored different kinds of unit tests.