dependency injection with xunit tests
By Per Fröjd
- csharp
- snippets
Unit testing with .NET 5+
I am not a fan of mocks, which is probably just something I need to get used to, but when faced with the task of building unit tests for services I tend to fall in the same trap every time. How do I properly set up the dependency-injection containers for .NET. So here’s one way of doing it.
This is specifically for Xunit-tests, but I’m fairly sure the other test suites have similar ways of handling it. We start by creating a Fixture
-class for our unit tests:
public class SecretFixture {
public SecretFixture() {
Config = new ConfigurationBuilder()
.AddUserSecrets<SecretFixture>()
.AddEnvironmentVariables()
.Build();
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(Config);
ConfigureService(serviceCollection, Config);
SetupConfiguration(serviceCollection, Config);
ServiceProvider = serviceCollection.BuildServiceProvider();
}
public ServiceProvider ServiceProvider { get; }
public IConfiguration Config { get; set; }
private void ConfigureService(IServiceCollection serviceCollection,
IConfiguration configuration) {
new SecretProject.Services.Setup().ConfigureServices(serviceCollection, configuration);
new SecretProject.ServiceContext.Setup().ConfigureServices(serviceCollection, configuration);
}
private void SetupConfiguration(IServiceCollection serviceCollection,
IConfiguration configuration) {
serviceCollection.Configure<ProjectConfiguration>(configuration.GetSection("project"));
serviceCollection.Configure<AppSettings>(configuration);
}
}
So I brought it all down at once, because it seems more complicated to split it up rather than looking at the whole picture at once.
First piece of the puzzle is to construct the same configuration as the actual application would require, because most of the projects in this Solution relies on certain secrets in order to be initialized. So we call the ConfigurationBuilder
which is just another part of the Microsoft.Extensions.Configuration
-package. We shove in our secrets defined via the dotnet
CLI and then the environment variables, before building the Configuration
object.
Remember in order for the project to recognize the dotnet
secrets, they have to be added to the Test-project in the solution, because the secrets are only added on a project-basis, not solution-basis.
Following that, we start building up our ServiceCollection
, with our handy help-methods. In each of the projects in this solution, there’s a Setup.cs
file which handles the dependencies specific for that project, which makes it handy for us to just call that file, to populate our serviceCollection with the interfaces and the instances. Lastly, we do some configuration binding to our configuration-classes, from the UserSecrets and EnvironmentVariables.
Bear in mind, we’re building a single Fixture for each of the tests, but you don’t have to do it this way. Since all tests are split by the unit they’re trying to test, it might be wiser (but more work) to build a specific Fixture for each unit you’re going to test, so that only the dependencies of that particular unit, is built up. But in this case, I’m lazy, and sharing one Fixture for all my unit tests.
So next up in line, we need a final piece of the puzzle (there wasn’t that many pieces..), to start using the DI:
public class UserServiceTest: IClassFixture<SecretFixture> {
private readonly IServiceContext _context;
private readonly ServiceProvider _serviceProvider;
private readonly IUserService _userService;
public UserServiceTest(SecretFixture fixture) {
_serviceProvider = fixture.ServiceProvider;
_context = _serviceProvider.GetService<IServiceContext>();
_userService = _serviceProvider.GetService<IUserService>();
}
[Fact]
public void Test_Can_Check_For_Permission() {
var userId = 3;
var secretId = 2;
var badUserId = 2;
_context.RunInTransaction(() => {
var shouldBeTrue = _userService.CanAccessSecret(userId, secretId);
Assert.True(shouldBeTrue);
var shouldBeFalse = _userService.CanAccessSecret(badUserId, secretId);
Assert.False(shouldBeFalse);
});
}
}
This looks pretty close to how we deal with DI in the real application, but instead of having all of the instances injected through our constructor, our fixture is passed through to it. And since we’ve already built up our serviceCollection, we can simply call .GetService<Interface>
to fetch each of the instances from our collection. Once that’s all done, we’re good to go to start consuming these services in our tests.