'xUnit - Unable to resolve service from Service Scope (NullReferenceException)
Without wanting to immediately depend on another nuget package (Xunit.DependencyInjection), I tried to setup my IoC container manually by implementing a TestsBase class, which every single unit test class will inherit from. Throughout my project I'm using a CQRS pattern with Mediatr and FluentValidation. For both the ValidationTests and HandlingTests I'm respectively resolving the implemented IValidator type and the implemented IMediator type.
As every test class individually was running as expected I initially thought I worked out the setup of my TestsBase correctly. However, when trying to do a 'Run all Tests', it seems something goes wrong during the configuration of my container OR the disposal of my ServiceScope. When trying a 'Run all Tests' only 1 testclass will run correctly, and every other testclass will throw NullReferenceExceptions on every testmethod, particularly on the ServiceScope property.
I've already tried some different approaches, up to the point where multiple test classes would run correctly, but until now I've never managed to get a fully succesful 'Run all Tests'. I'm up to date with the concept of Fixtures within xUnit, but I don't feel I would get a lot of benefit from them trying to solve this issue.
Am I doing something completely wrong regarding the concepts of a ServiceContainer? Or is the locking mechanism not working as I would expect it to?
My current setup:
TestsBase:
public class TestsBase : IDisposable
{
private IServiceScope ServiceScope { get; set; }
protected readonly IUnitOfWork Uow;
protected TestsBase()
{
ConfigureServiceContainer();
ConfigureNBuilder();
Uow = ServiceScope.ServiceProvider.GetService<IUnitOfWork>();
// Wrap test with transaction
Uow.BeginTransactionAsync().GetAwaiter().GetResult();
}
private static bool _containerIsConfigured;
private static readonly object ContainerSyncObject = new object();
private void ConfigureServiceContainer()
{
lock (ContainerSyncObject)
{
if (!_containerIsConfigured)
{
var services = new ServiceCollection();
// FLUENTVALIDATION
services.AddValidatorsFromAssembly(typeof(ValidationBehavior<,>).Assembly);
ValidatorOptions.Global.LanguageManager = new CustomLanguageManager(); // Override the default validation messages
// AUTOMAPPER
services.AddSingleton<CrossCutting.Interfaces.IMapper, AutoMapperMapper>();
services.AddSingleton<AutoMapper.IConfigurationProvider>(AutoMapperConfiguration.ConfigureAutoMapper());
services.AddSingleton<AutoMapper.IMapper>(sp =>
new Mapper(sp.GetRequiredService<AutoMapper.IConfigurationProvider>(), sp.GetRequiredService));
// MEDIATR
// AddMediatR scans the assembly for potential handlers and registers them through DI
services.AddMediatR(typeof(UnitOfWorkHandler<,>).Assembly);
// Have MediatR call the Transaction Behavior on every request
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TransactionBehaviorForTests<,>));
// BUSINESSMANAGERS
services.AddTransient(typeof(IContactManager), typeof(ContactManager));
// DATABASE
var connectionString = "Data Source=(localdb)\\MSSQLLocalDb;Initial Catalog=brbsalon;Integrated Security=True;MultipleActiveResultSets=True";
services.AddScoped<IEntitiesDbContext>(x => new EntityFrameworkEntitiesDbContext(connectionString, true));
services.AddScoped(typeof(IGenericRepository<>), typeof(EntityFrameworkGenericRepository<>));
services.AddScoped<IUnitOfWork, EntityFrameworkUnitOfWork>();
ServiceScope = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true }).CreateScope();
_containerIsConfigured = true;
}
}
}
protected IValidator<T> ValidatorFor<T>()
{
try
{
Uow.DetachAllChangeTrackerEntitiesForTests(); // Clear dbContext BEFORE action
//return ServiceProvider.GetService<IValidator<T>>();
return ServiceScope.ServiceProvider.GetService<IValidator<T>>();
}
finally
{
Uow.DetachAllChangeTrackerEntitiesForTests(); // Clear dbContext AFTER action
}
}
protected IMediator GetMediator()
{
//return ServiceProvider.GetService<IMediator>();
return ServiceScope.ServiceProvider.GetService<IMediator>();
}
/// <summary>
/// Inserts an entity directly in the database (not through manager)
/// </summary>
public async Task<TEntity> InsertEntityAsync<TEntity>(TEntity entity)
where TEntity : class, IEntityBase, new()
{
await Uow.ExecuteActionWithSaveDbContextChangesAsync(() =>
{
Uow.RepositoryFor<TEntity>().Insert(entity);
});
return entity;
}
protected virtual void Dispose(bool disposing)
{
if (Uow != null)
{
// Rollback the transaction to keep the database clean
Uow.CloseTransactionAsync(new Exception("Dummy exception which will simulate rollback"));
Uow.Dispose();
}
lock (ContainerSyncObject)
{
if (_containerIsConfigured)
{
ServiceScope.Dispose();
_containerIsConfigured = false;
}
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
Example of one of my unit test classes:
public class GetUserDetailsValidatorTests : TestsBase
{
private readonly IValidator<GetUserDetails> _validator;
public GetUserDetailsValidatorTests() // constructor gets called BEFORE each test
{
_validator = ValidatorFor<GetUserDetails>();
}
[Fact]
public async Task Valid_For_ValidDto()
{
// Arrange
var dto = new GetUserDetails { UserId = Guid.NewGuid() };
// Act
var validationResult = await _validator.ValidateAsync(dto);
// Assert
validationResult.IsValid.ShouldBeTrue();
}
[Fact]
public async Task Invalid_For_EmptyDto()
{
// Arrange
var dto = new GetUserDetails();
// Act
var validationResult = await _validator.ValidateAsync(dto);
// Assert
validationResult.IsValid.ShouldBeFalse();
validationResult.Errors.ShouldContainValidationMessage(nameof(User), VALIDATION_NOT_EMPTY_VALIDATOR);
validationResult.Errors.Count.ShouldBe(1);
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
