I want to share a personal project I've been working on called BuddyInjector.
BuddyInjector is a lightweight dependency injector created to help with tests.
The main focus of BuddyInjector is to eliminate the boilerplate of instantiating each dependency of your class to write simple tests, keeping it very simple to override only what you need for your mocks.
In this post, I will show you an example of a common use case.
If you like to read the project documentation (it's very short), you can go directly to the GitHub repository.
Intro
To keep it simple, let's use a basic scenario. We have a Service that calculates a value and returns the result. This Services depends on two repositories, RepositoryA and RepositoryB. Getting a value from each repository, the Service sums the values and returns the value.
- The interface and the implementation of the repositories.
public interface IRepositoryA
{
int GetTotal();
}
public interface IRepositoryB
{
int GetTotal();
}
public class RepositoryA : IRepositoryA
{
public int GetTotal()
{
return 1_000;
}
}
public class RepositoryB : IRepositoryB
{
public int GetTotal()
{
return 500;
}
}
- The interface and the implementation of the Service.
public interface IService
{
int Total();
}
public class Service(IRepositoryA repositoryA, IRepositoryB repositoryB) : IService
{
public int Total()
{
return repositoryA.GetTotal() + repositoryB.GetTotal();
}
}
As you can see in the code, the repositories have a value hardcoded to simplify the example. So calling Total()
from Service
should return 1500, because RepositoryA
returns 1000 and RepositoryB
returns 500.
The test implementation without BuddyInjector
For the first example, let's create a normal unit test for Service.Total()
.
public class UnitTest1
{
[Fact]
public void Given_Real_Implmentations_With_Manual_Instantions_Should_Return_1500()
{
// Arrange
IRepositoryA repositoryA = new RepositoryA();
IRepositoryB repositoryB = new RepositoryB();
IService service = new Service(repositoryA, repositoryB);
// Act
int value = service.Total();
// Assert
Assert.Equal(1_500, value);
}
}
As you can see, we need to instantiate each class manually to test only a single method. So imagine if we add more dependencies to the Service class or if we would like to create multiple tests to the Total()
method.
BuddyInjector was created to help with this problem.
The test implementation using BuddyInjector
First, we need to register the dependencies into BuddyInjector. If you already use dependency injections in your projects, there is nothing new in this step.
At the register, we inform the type and the implementation. So it can be read as "When you find something that expects IRepositoryA
delivery an instance of RepositoryA
", for example.
BuddyInjector buddyInjector = new BuddyInjector();
buddyInjector.RegisterTransient<IRepositoryA, RepositoryA>();
buddyInjector.RegisterTransient<IRepositoryB, RepositoryB>();
buddyInjector.RegisterTransient<IService, Service>();
But repeating it on every test doesn't solve the problem presented in the first approach, right?
To solve it, BuddyInjector has the RegisterAll method. It allows us to write the dependencies instantiations once and reuse it through an Action delegate.
public Action<BuddyInjector> defaultInstances = buddyInjector =>
{
buddyInjector.RegisterTransient<IRepositoryA, RepositoryA>();
buddyInjector.RegisterTransient<IRepositoryB, RepositoryB>();
buddyInjector.RegisterTransient<IService, Service>();
};
BuddyInjector buddyInjector = new BuddyInjector().RegisterAll(defaultInstances);
Here is the new approach of the test using the BuddyInjector.
public class UnitTest1
{
public Action<BuddyInjector> defaultInstances = buddyInjector =>
{
buddyInjector.RegisterTransient<IRepositoryA, RepositoryA>();
buddyInjector.RegisterTransient<IRepositoryB, RepositoryB>();
buddyInjector.RegisterTransient<IService, Service>();
};
[Fact]
public void Given_Real_Implmentations_With_BuddyInjector_Instantions_Should_Return_1500()
{
// Arrange
using BuddyInjector buddyInjector = new BuddyInjector().RegisterAll(defaultInstances);
IService service = buddyInjector.GetInstance<IService>();
// Act
int value = service.Total();
// Assert
Assert.Equal(1_500, value);
}
}
Cool, right?
And now, to finish the example, let's see how to override using the simplicity of the BuddyInjector.
For this example, let's change the value returned by RepositoryB
. Originally it returns 500, but for our example, we'll substitute it for 900.
To substitute the object creating a mock, we'll use the NSubstitute package. This article will not cover how to use the NSubstitute, but to explain the basics we say to the code "When someone calls method X, returns the value Y", so the result is a fake value.
public class UnitTest1
{
public Action<BuddyInjector> defaultInstances = buddyInjector =>
{
buddyInjector.RegisterTransient<IRepositoryA, RepositoryA>();
buddyInjector.RegisterTransient<IRepositoryB, RepositoryB>();
buddyInjector.RegisterTransient<IService, Service>();
};
[Fact]
public void Given_Mock_Implmentations_With_BuddyInjector_Instantions_Should_Return_1900()
{
// Arrange
var repositoryBMock = Substitute.For<IRepositoryB>();
repositoryBMock
.GetTotal()
.Returns(900);
using BuddyInjector buddyInjector = new BuddyInjector().RegisterAll(defaultInstances);
// Override the injection of only what you need.
buddyInjector.RegisterTransient<IRepositoryB>(() => repositoryBMock);
IService service = buddyInjector.GetInstance<IService>();
// Act
int value = service.Total();
// Assert
Assert.Equal(1_900, value);
}
}
In this last example, we registered all the original classes and substituted only what we needed for the test, which was the IRepositoryB
. So the BuddyInjector overrated the first register for the last register created buddyInjector.RegisterTransient<IRepositoryB>(() => repositoryBMock);
It'll work only for the specific test. When the scope of the tests is finished, the BuddyInjector will close the scope too, disposing everything that needs it, and start a new object on the next tests.
Photo by Markus Spiske on Unsplash