Published on

Unit Testing Best Practices

Authors

Introduction

This article was originally published on medium.com. Link to original.

It is often said that the quality of the code you write for your tests should be at least of the same quality as production code.

Although I agree with above statement, I believe that programming styles and patterns for tests need to be different from production code.

Why? Because we expect different things from tests and production code.

  • We want tests to be self-explained. If a test fails, we don't want to navigate a long way to understand it. We are usually busy doing something else.

  • We hate cascade-failing tests. You might have experienced something like this:

I've just added argument X to method Y in class Z. I understand why these 2 tests are failing. However, I'll spend the next 3 hours fixing those 47 failing tests even though my change is not even related.

  • We want/need to minimize test maintenance burden to keep our tests going. There is an inherent strong pressure over tests because they are not features users love. Ultimately, we don't want somebody (or ourselves) asking "why do we even bother writing tests?"

Developers spend hundreds of hours writing unit tests because its value has been proven over the years. Authors endorse the activity. TDD and high percentage of code coverage are common these days. However, more often than not, developers end up writing a huge pile of unmaintainable crap.

How to write better tests

There are several patterns that can make your tests maintainable. I highly recommend reading "Working Effectively with Unit Tests" by Jay Fields. Here's a quick summary of some of the ideas discussed in the book:

Don't use loops in your test code

Replace them with individual tests. Once any of them fail, you'll be able to find the root cause easily.

In-Line Setup

DRY is good. Yes, but understanding a test at first glance is ever better.

Prefer In-line setup in each test over Setup method.

Having everything under a common setup method, usually leads to issues such as:

  • Not all initialization logic is actually used for all tests.

  • Moving up and down thru a file to track initialization values is time consuming and we lose focus on the task at hand.

  • Changing Setup method, may break several tests you are not interested in.

If you think that your initialization logic is getting too complex, see next topic.

Data Builder

If you are creating a few instances of a Class Under Test and its dependencies are simple enough, just use a plain "new" to create and initialize them.

If you find yourself writing complex logic to create objects, it may be worth creating an Data Builder. See this post for details.

Assert Last

You may be familiar with AAA (Arrange-Act-Assert) already. The last A happens to be the most important one. Even if you can't go full AAA, your tests will be more readable and maintainable if you adopt Assert Lasts pattern.

Assert Throws

Once you have adopted Assert Last, applying that rule for tests that expect exceptions might be tricky if your Unit Test frameworks don't support it.

Some frameworks support ExpectedException decorators that you can apply to your test methods:

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void MyTest()
{
	// Arrange
	var x = 12;
	var y = 17;

	// Act
	ClassUnderTest.Process(x, y);
	
	// Assert???
}

…but it violates the Assert Last rule.

Ideally, we would need something as follows:

 [TestMethod]
 public void MyTest()
 {
   // Arrange
   var x = 12;
   var y = 17;

   // Act
   Action actionThatShouldThrowException = () => ClassUnderTest.Process(x, y);

   // Assert
   Assert.That.Throws<ArgumentOutOfRangeException>(actionThatShouldThrowException);
}

In case the framework of your choice doesn't support it, you can implement Assert.Throws yourself with little effort. Example

Expect Literals

Expected values should be as static as possible. Asserting a literal such a string or number is a best practice because it helps in focusing our attention on the actual value. Jay warns us about the common anti-pattern of storing literal values in a variable:

The expected value should be a literal itself, not a variable holding a value that was previously created by a literal

// WRONG - we need to focus our attention on both expected AND actual
var expected = this.ComputeExpectedValue();
Assert.AreEqual(expected, actual);

// WRONG - we still need to know about expected value
var expected = "testValue";
// .. some other code
Assert.AreEqual(expected, actual);

// GOOD
Assert.AreEqual("testValue", actual);

Avoid Overspecification

Keep your tests as simple as possible. They should be focused on a single functionality. It is OK to have a few tests that collaborate with multiple objects but there should be a limited number of those in your test suite. Most of your tests should be really simple and interact with a single object. Law of Demeter applies here as well.

Watch your ROI

Don't go writing tests blindly without thinking why. Evaluate each test to write from a ROI point of view. Even if you could write a test to validate every feature, it wouldn't worth the effort.

Aiming at high code coverage is important but 100% is impractical. If you reach 100% code coverage it means, among other things, that you might have written a lot of negative ROI tests

Conclusion

Writing effective Unit Tests is hard. Doing it wrong is very counterproductive to both team efficiency and motivation. Jay Fields does a great job at showing some guidance in his book "Working Effectively with Unit Tests" about what works better in the long run. We need to be aware that what's a best practices for production code might an anti-pattern for test code.