Maintainable test setup and cleaner tests

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.


[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
}

view raw

Tests.cs

hosted with ❤ by GitHub

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):


private TestedObject testedObject;
[SetUp]
public void Setup()
{
testedObject = newTestedObject();
}

view raw

TestSetup.cs

hosted with ❤ by GitHub

And tests are modified to just exercise object under test that is created somewhere else.


[Test]
public void It_does_this()
{
var result = testedObject.DoThis();
// assert result
}
[Test]
public void It_does_that()
{
var result = testedObject.DoThat();
// assert result
}

view raw

Tests2.cs

hosted with ❤ by GitHub

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:


[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:


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());
}

view raw

NewSetup.cs

hosted with ❤ by GitHub

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.


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
}

view raw

CreateSut1.cs

hosted with ❤ by GitHub

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.


[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.


public TestedObject CreateSut(IDependency dependency = null)
{
return new TestedObject(depency);
}

view raw

CreateSut1.cs

hosted with ❤ by GitHub

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?


[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()


public TestedObject CreateSut(IDependency dep = null, IDependency2 dep2 = null)
{
return new TestedObject(
dep ?? Mock.Of<IDependency>(),
dep2 ?? Mock.Of<IDependency2>());
}

view raw

CreateSut2.cs

hosted with ❤ by GitHub

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

Jedna uwaga do wpisu “Maintainable test setup and cleaner tests

Dodaj komentarz