The Magic Tricks of Unit Testing

Stephen Meriwether
4 min readOct 29, 2017

None of these ideas are ones that I came up with, this post is based on The Magic Tricks of Testing by Sandi Metz which you should definitely watch when you have some time.

This post is specifically about Unit Tests.

TLDR:

The Current Testing Experience:

In my experience with working on code and through talking with others, most people don’t like their tests. They are slow, fragile, and expensive. But it doesn’t have to be this way! Unit tests are an integral part of a testing suite and they can be thorough, stable, fast, and few by following a few simple rules.

Before we go over the rules of unit testing, we need to think about the different types of messages and the objects we are trying to test.

Objects are simple!

There are 2 different message types: Query and Command.

Query Messages:

  • return something
  • change nothing
def sum(a, b)
a + b
end

Query messages are the easiest messages to test. Given some input, they return some result, without affecting any other part of the system.

Command Messages:

  • return nothing
  • change something
def set_position(x)
@position = x
end

Command messages have some side-effect, they do something that can affect other parts of the system.

Messages are either command, query, or both

Now that we understand they different types of messages we need to understand how messages interact with an object. From the perspective of an object, a message is either: incoming, outgoing, or to self.

Incoming Messages:

Our object is the receiver, not the caller.

class Adder
def add(x, y)
x + y
end
end

adder = Adder.new
adder.add(1, 2) # 3

In this example our adder object is receiving an incoming add message.

Outgoing Message:

class Math
def initalize(adder = Adder.new)
@adder = adder
end

def add(x, y)
@adder.add(x, y) # Outgoing message
end
end

@math = Math.new
@math.add(1, 2) # 3

In this example, the add method on our @math object is sending an outgoing message to the @adder object.

Self Messages:

class Math
def initalize(adder = Adder.new)
@adder = adder
end

def average(numbers)
sum(numbers) / numbers.length
end

private

def sum(numbers)
numbers.inject(0) do |sum, num|
@adder.add(sum, num)
end
end
end

In this example our average method is calling a private sum method.

Rules of Testing:

  1. Testing incoming query messages by making assertions about what they send back.

Test the interface not the implementation. When dealing with an incoming query method we simply need to write assertions to verify the output, given some input. Simplicity is key.

2. Test incoming command messages by making assertions about direct public side effects.

It is the sole responsibility of the receiver for asserting the result of direct public side effects. Direct being the things close to the object that change and Public being the things that may affect other parts of the application.

3. Do not test private methods

Do not make assertions about their return values. Do not assert that they are called. These tests are redundant. The tests for our public API, when done correctly, will properly test our private methods.

Sometimes practicing TDD on a particularly complicated private method can be helpful and by all means test away! Just delete them once you are done :).

4. Do not test outgoing query messages

Do not make assertions about their results. Do not assert that they are called. If a message has no visible/public side effects the sender should not test it. The tests are also redundant. Outgoing query messages are already tested by the receiver.

5. Expect to send outgoing command messages

If we send a message that we expect to change something (create a side-effect) then we need to make sure that method is called. We don’t need to test the side effect (once again redundant) but we need to make sure the method is called.

Lets think about this a bit more. Why do we need to assert that outgoing command messages are called but not outgoing query messages?

If an outgoing query message changes so will the result of our method, failing our test. In the example above, if the add method on the adder object suddenly became x - y then our Math#average result will change and our test will fail.

An outgoing command message has no impact on the result of our method. If its details change, the result of our method will not change, leaving us with broken code and a green test. Because of this we need to do a bit more, and asserting that the message was sent is enough.

6. Honor the contract

In Rule #7 I said that we need to “assert that outgoing command message are sent”. The simplest way to do this is via Mocks. In a traditional sense Mocks are fragile, if the API of the outgoing message changes we end up with a green test suite and a broken application. Modern testing frameworks provide mechanisms to codify that a contract is honored.

See here for an example of how to do this using Spec.

Summary

The 6 Rules of Unit Testing are more “guidelines”. They will work for nearly all situations you might come across but as you become more confident in writing Unit Tests you will become better at knowing when they can and maybe should be broken.

  • Be A Minimalist
  • Use Good Judgement
  • Test Everything Once
  • Test the Interface
  • Trust Collaborators
  • Insist on Simplicity
  • Practice

--

--