A Testing Convention for Fixie with Dependency Injection support
I utilize Fixie for my dotnet core unit and integration testing.
"Fixie is a .NET modern test framework similar to NUnit and xUnit, but with an emphasis on low-ceremony defaults and flexible customization",
The goals of the testing convention are:
- Support consistent paradigm by using constructor injection.
- Tell Fixie how to find the tests.
- Tell Fixie how to instantiate the tests.
- Tell Fixie how to execute the tests.
How to find the tests.
This is easy we inherit from the default Discovery class and then add a Where
delegate. The delegate allows us to not treat methods named "Setup" as tests.
public class TestingConvention : Discovery
{
public TestingConvention()
{
Methods.Where(aMethodExpression => aMethodExpression.Name != nameof(Setup));
}
}
Support consistent paradigm by using constructor injection.
Dependency Injection is central to dotnet core. We configure the DI container in Startup.cs and then write our code with explicit dependencies called out in the constructor and don't think anything of it.
I want to write my test classes the same way. If a test class has a test dependency I can inject it. To accomplish this we create a ServiceCollection
,
configure it with the all the test cases. Then build the ServiceProvider
.
public TestingConvention()
{
var testServices = new ServiceCollection();
ConfigureTestServices(testServices);
ServiceProvider serviceProvider = testServices.BuildServiceProvider();
ServiceScopeFactory = serviceProvider.GetService<IServiceScopeFactory>();
}
private void ConfigureTestServices(ServiceCollection aServiceCollection)
{
// Register any dependencies you need here.
// Scrutor Nuget package gives us the "Scan" method.
aServiceCollection.Scan(aTypeSourceSelector => aTypeSourceSelector
.FromAssemblyOf<TestingConvention>()
.AddClasses(action: (aClasses) => aClasses.TypeName().EndsWith("Tests"))
.AsSelf()
.WithScopedLifetime());
}
How to execute the tests.
To change the default execution in fixie we implement the Execution
interface.
To utilize the DI container, instead of creating an instance directly we let the service provider create it for us.
To support the ability to have Setup run after construction we use reflection to find the method and execute it.
public void Execute(TestClass aTestClass)
{
aTestClass.RunCases(aCase =>
{
using (IServiceScope serviceScope = ServiceScopeFactory.CreateScope())
{
object instance = serviceScope.ServiceProvider.GetService(aTestClass.Type);
Setup(instance);
aCase.Execute(instance);
}
});
}
private static void Setup(object aInstance)
{
System.Reflection.MethodInfo method = aInstance.GetType().GetMethod(nameof(Setup));
method?.Execute(aInstance);
}
So now fixie can find instantiate and execute our tests using DI.
The code
Credits: Patrick Lioi @plioi