Apr 12, 2011

Unit testing classes with a lot of dependencies

I’m sure every TD developer will scream that it’s not right to have classes with a lot of dependencies. I agree with you, but it happens. It happens for example in MVP pattern for ASP.NET, or for controllers in MVC.

Why it happens? The main reason I think are unnecessary abstractions. Many applications have interface like IRepository<T> : where T : IEntity. Such interface seams to be very useful. It gives you strongly typed access to the entities, and gives you ability to put common methods there (like Get<T>(int id)). Everything looks good so far. We configure our application to use IoC container and don’t bother with dependencies:

public ProductsController : Controller
{
    private readonly IRepository<Product> productsRepository;

    public ProductsController(IRepository<Product> productsRepository)
    {
        this.productsRepository = productsRepository;
    }

    public ActionResult Index()
    {
        var products = productsRepository.GetAll();
        return View(products);
    }
}

Everything looks good so far and we are able to write very simple unit test for it. Its easy to mock GetAll() method and verify that View.Model contains exact the same collection that returned from repository.

But then you need to create a Create method that requires getting list of all categories that could be assigned to product. And all of a sudden you have two dependencies:

public ProductsController : Controller
{
    private readonly IRepository<Product> productsRepository;
    private readonly IRepository<Category> categoriesRepository;

    public ProductsController(IRepository<Product> productsRepository, IRepository<Category> categoriesRepository)
    {
        this.productsRepository = productsRepository;
        this.categoriesRepository = categoriesRepository;
    }

    public ActionResult Index()
    {
        var products = productsRepository.GetAll();
        return View(products);
    }

    public ActionResult Create()
    {
        var categories = categoriesRepository.GetAll();
        ViewBag.Categories = categories;
        return View();
    }
}

Its still looking good. But only if you have 2-3 tests that are using ProductsController constructor with 1 parameter. When you have 20 of those its becoming a nightmare to add new dependency in constructor.

I do understand that its not best example of why a lot of dependencies happens, but its real one. Its very easy to have up to 4-6 decencies in constructor. And to do with it in tests? Each test needs to set 2 or 3 of them and he doesn’t cares about other. My first approach was creating a helper methods as such:

public ProductsController GetController(IRepository<Product> productsRepository)
{
    return new ProductsController(productsRepository, Mock.Of<IRepository<Category>>);
}

Its perfectly works for classes with less then 3 dependencies. But even there the problem still exists. Consider mocking of current HttpContext.Current.User.Identity. It may happen that each controller action needs access to it, but most of tests doesn’t care what is going to be returned as current user. You will need to copy paste mocking code in each of such methods, or put some complex chain of methods calling each other to get default user for all test controllers.

What I prefer to do now is having a simple factory for controllers (presenters) in test class:

internal class ProductsControllerFactory()
{
    public ProductsControllerFactory()
    {
        this.CategoriesRepsitory = Mock.Of<IRepository<Category>>();
        this.ProductsRepository = Mock.Of<IRepository<Product>>();
        
        // you can put all default stubs here
    }

    public ProductsController GetController()
    {
        return new ProductsController(this.CategoriesRepository, 
                                      this.ProductsRepository);
    }

    public IRepository<Category> CategoriesRepsitory { get; set; }

    public IRepository<Product> ProductsRepository { get; set; }
}

So you end up with having constructor call in one place. Adding a dependency won’t affect existing tests, it will affect only controller factory. Each test can easily set dependency it needs and ignore dependencies it doesn’t cares about.

No comments:

Post a Comment