Revolutionize your .NET testing strategy by mastering shared context in Xunit. This expert guide for C# developers reveals cutting-edge techniques to amplify test suite efficiency, offering a blueprint to scalable, maintainable testing practices.
Today we will find out how to use the shared context with the xUnit library!
Developers must have to pay the same attention during coding than testing, mainly on subject like instantiation and memory recycling. xUnit gives all the tools to do this properly.
Let's get back to our goats.
To continue to improve our services, we will try to test the GoatSharedService
properly. It simply contains a FindGoatsByPattern
method to retrieve goats from the database matching the model.
public class GoatSharedService : IDisposable
{
private readonly GoatSharedDbContext _dbContext;
public GoatSharedService(GoatSharedDbContext dbContext)
{
_dbContext = dbContext;
}
public IEnumerable<string> FindGoatsByPattern(string pattern)
{
return _dbContext.GetGoatsName().Where(_ => _.Contains(pattern));
}
public void Dispose()
{
// Currently empty, but if we had to dispose something, we would do it here
}
}
In order to get the information, GoatSharedDbContext
contains the real DbContext
to query the database. The dbContext contains a "Goats" table containing all the goats.
public class GoatSharedDbContext
{
private readonly DbContext _dbContext;
public GoatSharedDbContext(DbContext dbContext)
{
_dbContext = dbContext;
}
// DbContext contains one table "Goats" containing all goats information
public IEnumerable<string> GetGoatsName()
{
return _dbContext.Goats.Select(_ => _.Name).ToList();
}
}
Both classes are simple, to emphasize the context of xUnit.
So now let's use xUnit to test the service!
Handle context with Constructor and Dispose
The first thing to think about is instantiating the GoatSharedService
, since that is the one that needs to be tested.
It needs a GoatSharedDbContext
, directly connected to a database via DbContext
. As you know, in unit testing, you should avoid creating a connection to an external component. The database is external.
A quick way to solve this problem is to simulate the GoatSharedDbContext
to create a fake list of goats inside, excluding the real database.
⚠️ I won't debate here whether it's better to create a Fake or a Mock, because that's not the purpose of this article, but feel free to update it!
We know what we need to create.
A main rule of testing is to make sure that every test is INDEPENDENT. That's written in bold, for a reason. Mixing up different tests (e.g. having the first test populate a list, and the second test read that updated list) will have a terrible impact when the tests are parallelized.
Fortunately, xUnit uses the constructor of the tests class to instantiate the properties before each test. Inside, GoatSharedService is instantiated for each test in this way.
Disposing works the same way. Extend the test class from IDisposable
to implement the Dispose
method to dump objects after each test.
public class GoatSharedContextTests : IDisposable
{
private readonly GoatSharedService _service;
public GoatSharedContextTests()
{
var mockDbContext = new Mock<GoatSharedDbContext>();
var goatsNames = new List<string>() { "Jacky", "Becky", "Freddy" };
mockDbContext.Setup(_ => _.GetGoatsName()).Returns(_goatsNames);
_service = new GoatSharedService(mockDbContext.Object);
}
[Fact]
public void ShouldHaveCountThree_WhenRetrieveGoatsByPatternY()
{
var goats = _service.FindGoatsByPattern("Y");
goats.Should().HaveCount(3);
}
[Fact]
public void ShouldHaveCountTwo_WhenRetrieveGoatsByPatternE()
{
var goats = _service.FindGoatsByPattern("E");
goats.Should().HaveCount(3);
}
public void Dispose()
{
_service.Dispose();
}
}
The best way to validate this process is to define breakpoints in the constructor and layout method. Each breakpoint should be activated once per test.
We should take 1 minute to think about what we just did:
- Constructor allows us to instantiate the objects needed for the test context
- Dispose allows us to clean up these objects properly
Share context among the tests with fixtures
Let's assume that our instantiation elements can be heavy. The more we reduce this time, the shorter and more efficient the overall test time will be. If instantiation takes 200~300ms, this time will be multiplied by the number of tests.
The next question is: do we need to instantiate and clean up the properties for each test?
In this particular case, our tests do not update the database context or the service. It makes sense to instantiate only once before running all the tests, and to get rid of them once they are all done.
Fixtures are there to save us all!
public class GoatSharedFixture : IDisposable
{
public GoatSharedService Service { get; private set; }
public GoatSharedFixture()
{
var mockDbContext = new Mock<GoatSharedDbContext>();
var goatsNames = new List<string>() { "Jacky", "Becky", "Freddy" };
mockDbContext.Setup(_ => _.GetGoatsName()).Returns(goatsNames);
Service = new GoatSharedService(mockDbContext.Object);
}
public void Dispose()
{
Service.Dispose();
}
}
A fixture is a class instantiated only once during the test class, even before the first call of the constructor. It must also implement IDisposable
to be called after the last fixture. In a sense, it serves as an encapsulation for the entire test class.
To call a fixture from a test, it must be set in the constructor of the test class, then each property will be accessible from the variable.
In our case, the GoatSharedService
is now instantiated and disposed once, and accessible for all tests.
public class GoatSharedContextTests : IClassFixture<GoatSharedFixture>
{
private readonly GoatSharedService _service;
public GoatSharedContextTests(GoatSharedFixture fixture)
{
_service = fixture.Service;
}
[Fact]
public void ShouldHaveCountThree_WhenRetrieveGoatsByPatternY()
{
var goats = _service.FindGoatsByPattern("Y");
goats.Should().HaveCount(3);
}
[Fact]
public void ShouldHaveCountTwo_WhenRetrieveGoatsByPatternE()
{
var goats = _service.FindGoatsByPattern("E");
goats.Should().HaveCount(3);
}
}
Fixtures can also be very useful for tests that need more services and classes instantiated to work. If you need a database connection or a fake cache manager, it can be efficient to create them only once.
Summary
xUnit has a very simple way to instantiate a context for tests:
- Constructor is called before each test
- Dispose is called after each test
- Fixture allows to create a context called before the first test and arranged after the last one
Fixture can also be a fixture collection, I prefer not to use it to avoid sharing the context between test classes, but if you want to know more, here is the official documentation :
Have a goat day!
Join the conversation.