Scope Ninject Bindings to Individual Tests

One of the main reasons for using an IoC container like Ninject is to be able to inject different dependencies into your classes in production code and in test code. Usually you don't even need to use an IoC container in your tests; it's easier to explicitly instantiate the dependencies in your tests and pass them to the class you're testing. This gives you much more control over them.

Nevertheless, there are cases when the IoC container can be useful in test code as well:

  • You might need to use it because of the service locator (anti)pattern imposed on you by your own code base or a framework you're using.
  • You might want to use it when your classes have lots of dependencies and you don't want to manually instantiate them all.

Unlike in production code, where your bindings usually don't change for the lifetime of the application, in test code the bindings often change from test to test. Not only that; you also want the tests to be isolated from each other, therefore the bindings need to be scoped appropriately. Transient scopes of course will work fine, but longer lived scopes, such as singleton, will quickly create problems.

In this post I'll demonstrate that objects, which are singleton scoped in production code, should usually be scoped to individual tests in test code. I'll be using Ninject as the IoC container and NUnit as the testing framework, but it shouldn't be difficult to modify the code for other frameworks, since the concepts are very similar in all of them.

The sample is based around a simplified repository interface:

public interface IRepository<T>
{
    IQueryable<T> Get();
    void Insert(T item);
}

For the purpose of the tests, we're going to use an in-memory implementation:

public class InMemoryRepository<T> : IRepository<T>
{
    private readonly List<T> _items = new List<T>();

    public IQueryable<T> Get()
    {
        return _items.AsQueryable();
    }

    public void Insert(T item)
    {
        _items.Add(item);
    }
}

Of course, we'll want to use the same instance of the class within a single test, and a different instance of the class for each test. This way we will achieve persistence within the test and isolation between tests. We could achieve that by manually creating the instance inside the test and pass it to any objects that depend on it. To make the scenario more complicated, we will assume that the object being tested is using the service locator pattern.

We're going to store users in our repository:

public class User
{
    public Guid Id { get; set; }
    public string Username { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }

    public User(string name, string surname)
    {
        Id = Guid.NewGuid();
        Name = name;
        Surname = surname;
    }

    public User(string name, string surname, string username)
        : this(name, surname)
    {
        Username = username;
    }
}

We will test a class which can generate a unique username for a new user:

public class UsernameGenerator
{
    public string GenerateUsername(User user)
    {
        var userRepository = NinjectKernel.Instance.Get<IRepository<User>>();
        for (int surnameChars = 1; 
            surnameChars <= user.Surname.Length; 
            surnameChars++)
        {
            var username = user.Name.ToLower()
                + user.Surname.Substring(0, surnameChars).ToLower();
            if (!userRepository.Get().Any(u => u.Username == username))
            {
                return username;
            }
        }

        throw new Exception("Couldn't generate username.");
    }
}

As you can see; the repository is not passed into the class through the constructor. Instead, the method requests it from the global kernel instance. NinjectKernel is a static class, containing the singleton instance of the kernel:

public static class NinjectKernel
{
    public static readonly IKernel Instance = new StandardKernel();
}

We only need two tests to demonstrate, how inappropriate scoping can break isolation between tests:

[Test]
public void AvoidDuplicates()
{
    var userRepository = NinjectKernel.Instance.Get<IRepository<User>>();
    userRepository.Insert(new User("Peter", "Patterson", "peterp"));
    var user = new User("Peter", "Parker");
    var usernameGenerator = new UsernameGenerator();

    var username = usernameGenerator.GenerateUsername(user);

    Assert.AreEqual("peterpa", username);
}

[Test]
public void DefaultUsernameWhenEmptyRepository()
{
    var user = new User("Peter", "Parker");
    var usernameGenerator = new UsernameGenerator();

    var username = usernameGenerator.GenerateUsername(user);

    Assert.AreEqual("peterp", username);
}

For the tests to pass, we need a binding for IRepository<User>. Transient binding is not going to work because in the first test we get the instance from the kernel both in the test and in the method which we are testing. The instance needs to be the same in both cases. By using singleton scope we can ensure this same instance. But now the second test will fail if they are run in the same order as they are defined above, because in the second test the repository will already contain the user from the first test and cause the username generator to give a different result.

We could avoid that by clearing the repository or by releasing the singleton before each test:

[SetUp]
public void Setup()
{
    var instance = NinjectKernel.Instance.Get<IRepository<User>>();
    NinjectKernel.Instance.Release(instance);
}

This is quite error prone since it needs to be done in every tests class for each applicable dependency.

A much better solution is to avoid the singleton scoping altogether and bind the objects to the scope of individual tests in the first place. Ninject supports binding to custom scopes; it only needs a function which returns the same value within the desired scope and a different one for any call outside of that scope. To implement such a method we can take advantage of NUnit's TestContext class which provides information about the currently running test. By combining these two together we can bind the repository as follows:

Bind<IRepository<User>>().To<InMemoryRepository<User>>()
    .InScope(ctx => TestContext.CurrentContext.Test.FullName);

In my opinion it's still better to avoid using IoC containers in tests, but when you really need to use them, this makes it at least more manageable than manually reinitializing all dependencies before each test and hunting down the cause for randomly failing interdependent tests, when you forget to do it.

Copyright
Creative Commons License