I've been trying to shift myself into a more test driven methodology when writing my .net MVC based app. I'm doing all my dependency injection using constructor-based injection. Thus far it's going well, but I've found myself doing something repeatedly and I'm wondering if there is a better practice out there.
Let's say I want to test a Controller. It has a dependency on a Unit Of Work (database) object. Simple enough... I write my controller to take that interface in its constructor, and my DI framework (Ninject) can inject it at runtime. Easy. Meanwhile in my unit test, I can manually construct my controller with a mocked-up database object. I like that I can write lots of individual self-contained tests that take care of all the object construction and testing.
Now I've moved on and started adding new features & functions to my Controller object. The Controller now has one or two more dependencies. I can write more tests using these 3 dependencies, but my older tests are all broken (won't compile), as the compiler throws a whole bunch of errors like this:
'MyProject.Web.Api.Controllers.MyExampleController' does not contain a constructor that takes 3 arguments
What I've been doing (which smells bad) is going back and updating all my unit tests, changing the construction code, and adding null parameters for all the new dependencies that my old tests don't care about, like this:
From this:
var controllerToTest = new MyExampleController(mockUOW.Object);
To this:
var controllerToTest = new MyExampleController(mockUOW.Object, null, null);
This gets everything to compile and gets my tests to run again, but I don't like the prospect of going back and editing tons of old tests just to change calls to my object constructors.
Is there a better way to write your unit tests (or a better way to write your classes and do DI?) so they don't all break when you add a new dependency?
You've run into a common issue when unit testing: duplication of code causing significant refactoring overhead. The solution is to try to restrict the instantiation of the object(s) under test to a single place. If you can, try to do it in the TestInitialize
method of your tests:
[TestInitialize]
public void Init()
{
this.mockUOW = new Mock<ISomeDependency>();
this.mock2 = new Mock<IAnotherDependency>();
this.mock3 = new Mock<IYetAnotherDependency>();
// Do initial set-up on your mocks
this.controllerToTest = new MyExampleController(this.mockUOW, this.mock2, this.mock3);
}
Sometimes, this isn't practical: you need to do specific set-up in each test before you create an instance of the class under test. In this situation, move the code to create the object into a well-named method, and call that:
[TestMethod]
public void MyTestMethod()
{
// Do any required set-up on mocks, etc.
this.CreateController();
}
private void CreateController()
{
this.controllerToTest = new MyExampleController(this.mockUOW, this.mock2, this.mock3);
}
Now you (hopefully) have only a single place in your tests to update when dependencies get added to one of your controllers.
Note also that that the CreateController
method isn't named CreateMyExampleController
. As a best practice, I try to avoid method names in tests that are specific to certain classes or methods. Here, for example, including the class name in the method adds another refactoring dependency which is easily overlooked.
You just inline the mock initialization. Instead of supplying null
just supply new Mock<IClassName>.Object()
.
You can try using automock (on codeplex) to automatically mock out your top-level objects, this reduces the amount of retyping that you need to fix objects.
Tests should not share the same context - you should be trying to test a stateless system. For the most part without refactoring tools enabled you're just going to have to deal with it.
Resharper is very powerful in this regard. When you add a new class, you can Ctrl+F6 and add a new parameter to a constructor. It will prompt you with how to fill caller locations - just type your mock in there and everywhere that it matters (if you are injecting all your dependencies the right way) will be filled in automatically.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With