BuddyInjector

Diego Faria - Aug 22 - - Dev Community

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode
  • 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();
        }
    }
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

. . .