The need to constantly update your tests whenever you change production code is one of the arguments against unit testing. Sure, when you do a big refactoring tests will need to change, but smaller changes should not make you change all the tests. I will try to help you with this issue. Specifically I will try to help you make your setup code easier to maintain.
Let’s get straight to the code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Test] | |
public void It_does_this() | |
{ | |
var testedObject = new TestedObjec(); | |
var result = testedObject.DoThis(); | |
// assert result | |
} | |
[Test] | |
public void It_does_that() | |
{ | |
var testedObject = newTestedObjec(); | |
var result = testedObject.DoThat(); | |
// assert result | |
} |
After writing second test most of us know that it’s good to extract creation of object under test into method. So in NUnit they write this (usually at the top of the file):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private TestedObject testedObject; | |
[SetUp] | |
public void Setup() | |
{ | |
testedObject = newTestedObject(); | |
} |
And tests are modified to just exercise object under test that is created somewhere else.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Test] | |
public void It_does_this() | |
{ | |
var result = testedObject.DoThis(); | |
// assert result | |
} | |
[Test] | |
public void It_does_that() | |
{ | |
var result = testedObject.DoThat(); | |
// assert result | |
} |
But is it much better? To me it’s not. When you read these tests do you know where and how is this testedObject set up? No, you have to jump to Setup(), read it, and then go back to the test. We’re using implicit setup here. Imagine there are many tests in file and you are reading the ones at the bottom of the file. You don’t see the setup. In my opinion, this is already problematic.
Then comes another requirement:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Test] | |
public void It_works_with_dependencies() | |
{ | |
var dependency = new Mock<IDependency>(); | |
// dependency setup | |
// how do I pass it to TestedObject? | |
} |
What often happens is this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private TestedObject testedObject; | |
private IDependency depency; | |
[SetUp] | |
public void Setup() | |
{ | |
var dependency = new Mock<IDependency>(); | |
// dependency setup | |
testedObject = new TestedObject(depency.object); | |
} | |
[Test] | |
public void It_works_with_dependencies() | |
{ | |
var result = testedObject.WorkWithDependency(); | |
// assert result | |
dependency.Verify(d => d.WasNotified()); | |
} |
Now we have to jump to Setup() to verify how testedObject was created, how dependency was set up and if it was injected at all? But there is other problem. Do you remember that these first two tests don’t need this dependency at all? Then why set it up and inject it into TestedObject? And when reading Setup() code can you tell what lines are needed for what tests without reading all tests? And it gets harder when there are more dependencies. Setup sometimes becomes the most complicated part of entire test class. It creates many dependencies, does some basic setup that is shared by all tests (are you sure it is without reading all the tests?) and then injects them all into constructor even though they are not needed in all test scenarios.
By the way, did you notice that [SetUp] methods are most of the time named Setup()? I know it’s setup, I see the attribute. 😉 Maybe help reader a bit and name it StartInMemoryDatabase() or CreateAnonymousCustomers() if that is what you are doing?
Different way
Let’s get back to first two tests but now we’ll setup differently.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public TestedObject CreateSut() | |
{ | |
return new TestedObject(); | |
} | |
[Test] | |
public void It_does_this() | |
{ | |
var sut = CreateSut(); | |
var result = sut.DoThis(); | |
// assert result | |
} | |
[Test] | |
public void It_does_that() | |
{ | |
var sut = CreateSut(); | |
var result = sut.DoThat(); | |
// assert result | |
} |
Right now maybe it’s not a big change. But now we’re using explicit setup. You can easily see where the testedObject is created and you can use your editors `Go to definition` feature to see how it is setup.
SUT stands for system under test. I like to use this convention because it makes tests easier to read – it’s easy to see what is tested and allows me to use my IDE’s`Navigate to method` feature. I think I first saw SUT in http://xunitpatterns.com/ – worth reading book!
Now to the test with dependency.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Test] | |
public void It_works_with_dependencies() | |
{ | |
var dependency = new Mock<IDependency>(); | |
// dependency setup | |
var sut = CreateSut(dependency.Object); | |
var result = testedObject.WorkWithDependency(); | |
// assert result | |
dependency.Verify(d => d.WasNotified()); | |
} |
To make this code compile we modify CreateSut() method.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public TestedObject CreateSut(IDependency dependency = null) | |
{ | |
return new TestedObject(depency); | |
} |
Code compiles and you didn’t need to modify any of the existing tests and their setups. All tests pass, World is beautiful!
And what if new dependency must be injected in next test?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Test] | |
public void It_works_with_another_dependency() | |
{ | |
var dependency2 = new Mock<IDependency2>(); | |
// dependency setup | |
var sut = CreateSut(dep2: dependency2.Object); | |
var result = testedObject.WorkWithDependency(); | |
// assert result | |
dependency.Verify(d => d.WasNotified()); | |
} |
To achieve this we just update CreateSut()
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public TestedObject CreateSut(IDependency dep = null, IDependency2 dep2 = null) | |
{ | |
return new TestedObject( | |
dep ?? Mock.Of<IDependency>(), | |
dep2 ?? Mock.Of<IDependency2>()); | |
} |
And again we didn’t need to modify any of the existing tests. With this approach each test clearly states which dependencies are used by specific feature and you can see how they are set up. Details that are not important for specific test are hidden. You don’t have to scroll between SetUp and test method and already existing tests are less likely to be changed whenever you change tested class. Notice that I also added some logic that creates mock if dependency is not passed. This might be usefull if you validate your objects in constructor.
I hope you find it useful.
Piotr
Widzę nowy design 🙂
Ładnie.
PolubieniePolubienie