.NET Unit Testing Entity Framework Core using Moq and AutoFixture
Unit Testing is an essential part of developing applications. Entity Framework Core is one of the most popular ORM's for connecting the database and the backend. These two components are commonplace in developing ASP.NET applications. Unfortunately, Unit Testing EF Core is not simple. How do you isolate EF Core and LINQ operations on a database without instantiating the database? Sure, we can connect to the database to test business logic and data outputs. But that means we have to deal with the headaches of permissions, possibly large data pulls, data that hasn't been created yet, etc.
Microsoft Documentation has an article that talks about mocking Entity Framework:
The part in the article I want to point to is the Create test context section where you mock DbContext. This code makes sense to test, but it's a pain to use this in production and to maintain unit tests long-term. And this code doesn't even address Task / Asyncs.
After extensive digging, I found a solution that's reasonable for unit testing EF Core. The libraries we need to use are:
- xUnit - Used for general unit testing.
- Moq - Used for mocking dependencies and EF Core's DbContext.
- AutoFixture - Used for creating mock data.
- MockQueryable - Implements the above Microsoft Documentation article in a sane way.
Setup
(If you want, you can skip this section and go straight to the examples)
Mangobytes
/Data
-> MangoBytesContext.cs
-> IMangoBytesContext.cs
/Models
-> Employees.cs
/Services
-> Employees.Service.cs
-> IEmployees.Service.cs
MangoBytes.Test
TestEmployeesService.cs
The packages we need to run these tests:
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Xunit
dotnet add package Moq
dotnet add package AutoFixture
dotnet add package AutoFixture.xUnit2
dotnet add package FluentAssertions
dotnet add package MockQueryable.Moq
The object model we will be using to run these tests:
using System;
namespace MangoBytes.Models
{
public class Employees
{
public int Id { get; set; }
public string Name { get; set; }
public string JobTitle { get; set; }
public int Salary { get; set; }
public DateTime DateHired { get; set; }
}
}
-- Mangobytes/Data/MangoBytesContext
using MangoBytes.Models;
using Microsoft.EntityFrameworkCore;
namespace MangoBytes.Data
{
public class MangoBytesContext : IMangoBytesContext
{
public MangoBytesContext(DbContextOptions<IMangoBytesContext> options)
: base(options)
{
}
public DbSet<Employees> Employees { get; set; }
}
}
and IMangoBytesContext (This is important)
-- Mangobytes/Data/IMangoBytesContext
using MangoBytes.Models;
using Microsoft.EntityFrameworkCore;
namespace MangoBytes.Data
{
public abstract class IMangoBytesContext : DbContext
{
public IMangoBytesContext()
{
}
public IMangoBytesContext(DbContextOptions<IMangoBytesContext> options)
: base(options)
{
}
public virtual DbSet<Employees> Employees { get; set; }
}
}
And lastly, our Service class, which we will be testing:
-- MangoBytes/Services/EmployeesService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MangoBytes.Data;
using MangoBytes.Models;
using Microsoft.EntityFrameworkCore;
namespace MangoBytes.Services
{
public class EmployeesService : IEmployeesService
{
private readonly IMangoBytesContext _context;
public EmployeesService(IMangoBytesContext context)
{
_context = context;
}
public async Task<List<Employees>> GetEmployees()
{
var results = await _context.Employees.AsNoTracking()
.ToListAsync();
return results;
}
public async Task<List<Employees>> GetEmployee(int id)
{
var results = await _context.Employees.AsNoTracking()
.Where(e => e.Id == id)
.ToListAsync();
return results;
}
public async Task<List<Employees>> GetEmployeeManager(int id)
{
var results = await _context.Employees.AsNoTracking()
.Where(e => e.Id == id)
.Where(e => e.JobTitle == "Manager")
.ToListAsync();
return results;
}
public async Task<List<Employees>> GetEmployeesNullTitle()
{
var results = await _context.Employees.AsNoTracking()
.Where(e => e.JobTitle == null)
.ToListAsync();
return results;
}
public async Task CreateEmployee(Employees employee)
{
_context.Employees.Add(employee);
await _context.SaveChangesAsync();
}
public async Task UpdateEmployee(Employees employee)
{
_context.Employees.Update(employee);
await _context.SaveChangesAsync();
}
public async Task DeleteEmployee(int id)
{
var employee = await _context.Employees.FindAsync(id);
_context.Employees.Remove(employee);
await _context.SaveChangesAsync();
}
public async Task<List<Employees>> GetEmployeeWithArgumentNullException(int id)
{
try
{
var results = await _context.Employees.AsNoTracking()
.Where(e => e.Id == id)
.ToListAsync();
return results;
}
catch (Exception)
{
throw new ArgumentNullException("Employee ID Not Found in Database");
}
}
}
}
The R in CRUD: Testing Employee Service Class
Now that we have our classes set up in the main project, let's write the Test Class Constructor and using statements:
-- MangoBytes.Test/TestEmployeesService.cs
using System.Linq;
using Xunit;
using Moq;
using AutoFixture;
using MockQueryable.Moq;
using FluentAssertions;
using MangoBytes.Services;
using MangoBytes.Models;
using MangoBytes.Data;
using System.Threading.Tasks;
using AutoFixture.Xunit2;
using System.Threading;
namespace MangoBytes.Test
{
public class TestEmployeesService
{
private readonly Fixture fixture;
private readonly Mock<IMangoBytesContext> _context;
private readonly EmployeesService _service;
public TestEmployeesService()
{
fixture = new Fixture();
_context = new Mock<IMangoBytesContext>();
_service = new EmployeesService(_context.Object);
}
-- More Code to come...
}
}
We want to initialize AutoFixture
in the constructor so we can use that library throughout the application without having to re-create it for every test. Next, we want to create a mock of IMangoBytesContext
and pass the Mock Object into Employee Service. This allows us to set DbSet<Employees>
and DbContext
.
With this Constructor created, we can write our first test. Let's do a simple get all employees statement:
With this Constructor created, we can write our first test. Let's do a simple get all employees statement:
[Fact]
public async Task Test_GetEmployees()
{
// Arrange
var employees = fixture.CreateMany<Employees>()
.AsQueryable()
.BuildMockDbSet();
_context.Setup(m => m.Employees).Returns(() => employees.Object);
// Act
var results = await _service.GetEmployees();
// Assert
results.Should().NotBeEmpty();
}
There's quite a bit going on here. Starting with the Arrange portion, what AutoFixture does is create objects with randomly generated values. fixture.CreateMany<Employees>()
creates 3 Employees rows in employees with all of the properties filled with GUID strings and random integers / floats. AsQueryable() and BuildMockDbSet() come from the MockQueryable.Moq library that allows us to create Mocks of DbContext DbSet. So all of this combined to create an Employee Object with records and mock it up so we can test the Service Class with DbContext. Pretty nifty.
Next, we want to setup DbContext and DbSet
Now we act on our Service Class by executing the GetEmployees() method and retrieve the results from it. Note that because the GetEmployees() method is an async Task that we have to await the method call and also declare our test method as async task.
Lastly, we assert the results. If you've never used FluentAssertions before, it's essentially the same as xUnit, but the assert syntax is narratively nicer and if tests fail the error messages have more clarity. results.Should().NotBeEmpty() is the same as xUnit's Assert.NotNull(results). This test checks if DbSet
With this setup in mind, this makes testing EntityFramework Core much easier. We don't have to seed fake data every time we test. We don't have to have a Database prepped with data in order to run an adequate integration test. We just want to test if we can get values out of our Service Class with as little headaches as possible.
When you run this Test in Debug Mode and you hover over results, you will see 3 Records with properties filled in. This is an example of you will see:
This shows the power of the AutoFixture library. We can spend more time writing solid tests and let the library fill in values that isn't relevant to care about.
Which is a good transition to the next test we want to do. Let's say we want to pull just a single Employee based on Id. All we have to do is make a couple of additions to the previous Test:
[Theory, AutoData]
public async Task Test_GetEmployee(int id)
{
// Arrange
var employees = fixture.Build<Employees>()
.With(c => c.Id, id)
.CreateMany()
.AsQueryable()
.BuildMockDbSet();
_context.Setup(m => m.Employees).Returns(() => employees.Object);
// Act
var results = await _service.GetEmployee(id);
// Assert
results.Should().NotBeEmpty();
}
The first change is the attribute [Theory, AutoData] above the test itself. Typically when we use [Theory] from xUnit, we expect something like [InlineData(64)] to come after it, which says we want to pass in the integer 64 into the test method and use it throughout the test. What the attribute AutoData does from AutoFixture.Xunit2 library is randomly generated a value that gets passed in for all parameters for that test method.
The next addition is .With(c => c.Id, id). When you declare With() after a Build(), that means you want every Id property in the Employees object to be equal to the id variable. Note that you don't have to use a variable; you can write in a literal integer or string (which we will see in the next example). I also want to note that this means we will have 3 records with the same Id. This will never happen in Production, but for the purpose of seeding data for a unit test, this is okay.
With all of these changes combined, we will test the GetEmployee(id) method, where we want to see if we can get back results using the randomly generated id. This test should pass.
I want to show two more quick examples with just doing read operations with AutoFixture and EntityFramework Core. Let's say we want to pull just values from the Employees table with the job title "Manager". We can add an additional With() statement that looks like this:
[Theory, AutoData]
public async Task Test_GetEmployeeManager(int id)
{
// Arrange
var employees = fixture.Build<Employees>()
.With(c => c.Id, id)
.With(c => c.JobTitle, "Manager")
.CreateMany()
.AsQueryable()
.BuildMockDbSet();
_context.Setup(m => m.Employees).Returns(() => employees.Object);
// Act
var results = await _service.GetEmployeeManager(id);
// Assert
results.Should().NotBeEmpty();
}
Or let's say values got inserted into the Employees table improperly and they have null for the JobTitle, and we want to pull those records. we can write this test:
[Fact]
public async Task Test_GetEmployeesNullTitle()
{
// Arrange
var employees = fixture.Build<Employees>()
.Without(c => c.JobTitle)
.CreateMany()
.AsQueryable()
.BuildMockDbSet();
_context.Setup(m => m.Employees).Returns(() => employees.Object);
// Act
var results = await _service.GetEmployeesNullTitle();
// Assert
results.Should().NotBeEmpty();
}
In both examples, we are writing tests akin to business rules. In the first example, we want to get Employees with a specific Id and a JobTitle of "Manager". In the second example, we can use the Without() statement to make JobTitle null and pull Employees with nulls as their JobTitle.
The C, U and D of CRUD
In the section above, we looked at read operations for EntityFramework Core and how AutoFixture helps us seed data so we can test these methods out in various scenarios. Now we want to see how we can test create, update and delete operations.
Let's start off with a simple create or Add statement for the Employees object. A run of the mill create operation involves an Employee record, an add statement and saving the changes to the database. So writing a test will look something like this:
[Fact]
public async Task Test_CreateEmployee()
{
// Arrange
var employee = fixture.Create<Employees>();
_context.Setup(m => m.Employees.Add(employee));
// Act
await _service.CreateEmployee(employee);
// Assert
_context.Verify(m => m.Employees.Add(employee), Times.Once);
_context.Verify(m => m.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
}
We start by creating an Employee using AutoFixture and then we use Moq's Setup() to Add() that employee to Employees. Note that we are not returning anything. Then we execute the CreateEmployee(Employee) method. In order to check that we successfully added this employee, we have to use Verify() on _context to test that these operations finished properly.
What the _context.Verify(m => m.Employees.Add(employee), Times.Once) line does is check that we added an employee exactly once. You will notice that if you type in Times after the comma, Intellisense should give you multiple options on what exactly you can verify on the mocked _context. If you have a method or set of operations where you are hitting an mocked object a certain amount of times or never, this is where you would check that.
And lastly, we also want to check that this record got saved to the database. The last line _context.Verify(m => m.SaveChangesAsync(It.IsAny
We can do the same thing for the Edit or Update portion of CRUD. This is another minimally viable unit test for Updates:
[Fact]
public async Task Test_UpdateEmployee()
{
// Arrange
var employee = fixture.Create<Employees>();
_context.Setup(m => m.Employees.Add(employee));
// Act
await _service.UpdateEmployee(employee);
// Assert
_context.Verify(m => m.Employees.Update(employee), Times.Once);
_context.Verify(m => m.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
}
And lastly the test for Deleting an Employee:
[Theory, AutoData]
public async Task Test_DeleteEmployee(int id)
{
// Arrange
var dbEmployee = fixture.Build<Employees>()
.With(c => c.Id, id)
.CreateMany(1)
.AsQueryable()
.BuildMockDbSet();
_context.Setup(m => m.Employees).Returns(() => dbEmployee.Object);
// Act
await _service.DeleteEmployee(id);
// Assert
_context.Verify(m => m.Employees.Remove(It.IsAny<Employees>()), Times.Once);
_context.Verify(m => m.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
}
Naturally, production code and unit tests will be a bit more involved than this, but this should be a good jumping off point to start writing unit tests for CRUD operations in EF Core.
Unit Testing EFCore and Exceptions
Once we have our main EF Core operation written, it's a good idea to wrap them in try / catch statements to handle for possible errors and exceptions. And in order to have good code coverage in our service class, we have to test these exceptions as well. However, unit testing exceptions isn't quite as intuitive as returning then asserting an object or verifying a mocked object. You have to create or force an exception and then test that exception. Let's take a look at this example:
public async Task UpdateEmployeeWithDbUpdateException(Employees employee)
{
try
{
var dbEmployee = await _context.Employees.AsNoTracking()
.FirstAsync(e => e.Id == employee.Id);
dbEmployee.JobTitle = employee.JobTitle;
await _context.SaveChangesAsync();
}
catch (Exception)
{
throw new DbUpdateException("Database unable to update employee record.");
}
}
This is a simple GET statement. But how do we test the catch (Exception) portion of this method? If we have a passing test, the catch statement will never be reached. Essentially, we have to write a failing test and return that exception and assert on that returned exception.
[Theory]
[InlineData(64, 32)]
public async Task Test_UpdateEmployeeWithDbUpdateException(int appId, int dbId)
{
// Arrange
var exceptionMessage = "Database unable to update employee record.";
var dbEmployee = fixture.Build<Employees>()
.With(c => c.Id, appId)
.CreateMany()
.AsQueryable()
.BuildMockDbSet();
var employee = fixture.Build<Employees>()
.With(c => c.Id, dbId)
.Create();
_context.Setup(m => m.Employees).Returns(() => dbEmployee.Object);
// Act
var ex = await Assert.ThrowsAsync<DbUpdateException>(() => _service.UpdateEmployeeWithDbUpdateException(employee));
// Assert
ex.Should().BeOfType<DbUpdateException>();
ex.Message.Should().Be(exceptionMessage);
}
In this example, we are purposefully having different two different Id for DbSet
Closing Thoughts
There is much more to unit testing EntityFramework Core than what's noted here. Code Coverage, If/Else Statements and Joins are a couple examples that I omitted from this article for brevity. I wanted to write this as a starting point to set up the tools required to adequately test the ORM. In the future, I'll go more in depth on some of these topics.
If you enjoyed this article and want to see more in the future, please feel free to support me by Buying me a Coffee.