How to write effective unit tests
Mar 28, 2016
rspec TDD Good Code

Unit testing is an essential practice in software delivery. It builds confidence that our code works and provides a safety net that enables us to change it safely. However, while well written tests provide us with traction and momentum, poorly designed tests often act as impediments that slow us down. This welcomes the question: what “good” unit tests look like? In this article we examine some key properties that separate the wheat from the chaff, driven by a simple example.

While working on a ruby project recently I came across some piece of test code similar to the following:

 1 describe ExpressionBuilder do
 2 
 3   describe "#build" do
 4     let(:expression) { double('Expression', children: []) }
 5 
 6     # several lines of code later...
 7 
 8     context "when selections are defined" do
 9       let(:selection_expression) { double('SelectionExpression') }
10 
11       it "should build a selection expression and append to expression's children" do
12         subject.instance_variable_set(@expression, expression)
13         expect(subject).to
14           receive(:build_selection_expression).
15           and_return(selection_expression)
16 
17         expect(subject.build).to eq expression
18         expect(expression.children.first).to eq selection_expression
19       end
20     end
21   end
22 end

Specify the “what” not the “how”

The reason I landed in this unit test in the first place was because I needed to know how to use ExpressionBuilder class to create expressions. Most often than not, that’s something I do when I need to learn how a particular class or method should be used. In that respect, this test doesn’t disclose clearly how the class should behave or how should be used.

For example, it is difficult to understand just by reading it how selections should be specified in the ExpressionBuilder class, or how those affect the expression returned to the user (its side effects). Tests like this are difficult to understand because they obscure the behaviour that is being tested. In short, they don’t act as documentation artifacts.

This becomes easily apparent when running the spec using rspec’s --format=documentation option, which outputs your test results in a human readable documentation format.

$ rspec spec/expression_builder_spec.rb --format=documentation

ExpressionBuilder
  #build
    when selection instance variable is defined
      should build a select expression and append it to expression's children

Finished in 0.14789 seconds
1 example, 0 failures

Note how the unit tests describes how the class keeps track of its selection expressions, not how clients interface with it.

When writing specs, always run them using this option to make sure they are readable.

The problem here is that instead of focusing on highlighting how the system under test behaves, the test focuses too much on how it is implemented, exposing implementation details irrelevant to its users.

If you are following TDD (which I hope you are), you should define your code’s specification in the form of a unit test even before writing a single line of code. This practice has the important implication of providing an explicit description of what your code is expected to do, how it is supposed to be used and how not to (this practice also engages you in an interface discovery process but that’s something to dwell in a separate article). In doing so you should adopt the mindset of the client. Focusing on how the system under test should behave instead of how it is tested transforms your test suite into a comprehensive and descriptive documentation artifact. In addition to validating its correctness, your tests should also answer the following concerning your code:

  1. How are users supposed to interact with it (interface)?
  2. Are there any relevant conditions which clients may rely upon remaining unchanged for the duration of the tested code’s execution? (invariants)
  3. Which are the conditions that should hold true before using it? (pre-conditions)
  4. What are its side-effects, what should users expect to happen by using it? (post-conditions)

The interface, invariants, pre-conditions and post-conditions are guarantees that your code provides to people using it and therefore are important to be understood to be used correctly. By making them explicit in your tests, you communicate effectively those guarantees to users and at the same time verify your code’s behaviour against them.

Another factor that contributes to the test’s obscurity is the fact that it does not make a clear connection between its data fixtures and verification logic. In this example the test uses a fixture named expression which is defined many lines before that (I have omitted code so its actual location was not the one shown in the excerpt). In result, the reader needs to navigate through the whole file in order to find its declaration to establish that connection and ultimately understand the test. This is a smell often called mystery guest and impedes the readability of your tests

Test the “what” not the “how”

Besides not documenting the code effectively the test is subject to another issue. Stubbing private members, such as in this case, makes your test tightly coupled to its implementation and therefore, susceptible to changes that are not supposed to alter the code’s externally published behaviour in any way. Any attempt for example to rename the private method build_where_expression or re-organize the logic of how the class tracks the expression’s elements will break it, potentially rendering refactoring a daunting task and discouraging developers from doing it. What you should be doing instead is testing its side effects.

Always test the behavior of your code, not its implementation.

Writing such tests is not always as easy as it sounds, but the real reason is not always obvious. Many programmers result in this kind of testing because its easier to do so. Don’t take me wrong; stubs and mocks are essential tools to isolate external collaborators tangentially related with the tested functionality. However, when placed on integral elements of the implementation logic (such as this case) they make your test suite intractable. Its maintenance cost increases and sooner than most people think developers avoid making value-adding changes that improve the codebase. You might want to check out Martin Fowler’s article for more insights on how to use stubs and mocks effectively.

Have you ever found yourself in a position where you didn’t do a seemingly small and harmless refactoring that would improve your codebase, just because of the sheer amount of tests that would break in effect?

If you find yourself in a situation that it is hard to test some piece of code, resist the convenience of stubbing and mocking private collaborators. Instead, address it as a smell that there might be something wrong with your design. Poorly designed code often lends itself to difficult to test code. Review your design especially for tight coupling as this is a common source of such problems.

Tightly coupled code is often difficult to test. When you come across this situation, consider refactoring your code.

Refactoring our test

We can now improve our original test by re-writing in a way that addresses some of the issues discussed earlier:

 1 describe ExpressionBuilder do
 2 
 3   describe '#build' do
 4 
 5     context "given a builder for an object with indexed attributes `foo` and `bar`" do
 6       before do
 7         class QueryableObjectMock
 8           include Indexable
 9 
10           indexed_attr :foo
11           indexed_attr :bar
12 
13           # ...
14         end
15 
16         @builder = QueryableObjectMock.expression_builder
17       end
18 
19       context "when i select attributes `foo` and `bar`" do
20         before { @builder.select('foo', 'bar') }
21 
22         it "should return expression `select foo, bar from QueryableObjectMock`" do
23           expression = @builder.build
24           expect(expression.to_s).to eq "select foo, bar from QueryableObjectMock"
25         end
26       end
27     end
28   end
29 end

Running our test now yields the following, more explanatory feedback regarding the behaviour of our class:

$ rspec spec/expression_builder_spec.rb --format=documentation

ExpressionBuilder
  #build
    given a builder for an object with queryable attributes `foo` and `bar`
      when i select attributes `foo` and `bar`
        should return expression `select foo, bar from QueryableObjectMock`

Finished in 0.14789 seconds
1 example, 0 failures

Conclusion

  • Treat your tests as documentation artifacts.
  • When writing unit tests, think about client interfaces, invariants, preconditions and postconditions. Try to make them explicit in the test.
  • Place fixtures within or near the test method to help readers establish the the cause and effect between them.
  • Focus your tests in the published behavior of your code, not its implementation.
  • Difficult to test code is often a symptom of poorly design code. Investigate for tightly coupled areas and refactor if necessary.
  • Remember, your test suite is also a (very important) part of your codebase and not a second-class citizen; it requires some love too!

Further reading

For a more in-depth discussion of this topic consider reading the book xUnit Test Patterns, Refactoring Test Code, by Gerard Meszaros (Addison-Wesley).

Share on