More Clean Tests: Handling Mocks & Block-based asserts

January 16, 2012 📬 Get My Weekly Newsletter

In a previous post, I talked about the overall structure of a test and how that was important to understand the test itself. A brief review:

  • Given - Establish the conditions under which the test will run
  • When - Run the code under test
  • Then - assert that the code did what you expect

This structure becomes problematic when using either mock objects or block-based asserts.

The Trouble with Mocks

When using mock objects in a test, you typically use a mocking framework (like mocha) to modify the behavior of objects the class-under-test collaborates with. You often test that the class-under-test made certain calls to its collaborators. Let’s look at an example.

Suppose we have an existing system and we wish to start recording some statistics, such as the number of times a method is called or how long a method takes to run. We’ve created a class, Statistics, that has some class methods on it to do the recording:

class Statistics
  # Add one to stat_name
  def self.count(stat_name)
    # ...
  end
end

We want to start using this class in our Salutation class to keep track of the number of times we’re calling #greeting. In order to add this in, we need to test that Salutation#greeting is calling Statistics.count. While we could set up a fake statistics server and examine it during our test, it’s more straightforward to use mocks.

class SalutationTest << TestCase
  def test_that_we_log_statistics
    saluation = Salutation.new(Person.new('David','Copeland',:male))
    Statistics.expects(:count).with('saluation.greeting.count')
    saluation.greeting
  end
end

What will happen is, if we don’t call Statistics.count("saluation.greeting.count") in the Salutation class, this test will fail. That’s what a mocking framework like mocha does for us.

Of course, there’s something odd about our test. There’s no call to any sort of assert method. The Given/When/Then is very unclear. For a real-world test that requires a lot more setup, it can be even more difficult to see what’s actually being tested. Essentially, the “Given/When/Then” is “out of order”:

class SalutationTest << TestCase
  def test_that_we_log_statistics
    # Given
    saluation = Salutation.new(Person.new('David','Copeland',:male))
    # Then
    Statistics.expects(:count).with('saluation.greeting.count')
    # When
    saluation.greeting
  end
end

Making our Intent Clear

We’d like to keep our test method in a canonical structure, or at least have some part of it follow the Given/When/Then structure. Unfortunately, our “Then”, the mock expectations, simply have to occur before the “When”. I think we can make it clearer, so let’s add a bit of code to help.

First, we’ll create a method named when_the_test_runs_then to clearly indicate that our expectations are part of our “Then”, and that they are going to be checked when the test runs, which happens later. We’ll also add a no-op method, assert_mocks_were_called that will allow our test to always have an assert and provide us with a way to be explicit about what’s being asserted. Although this “assert” method doesn’t do anything, it allows use to distinguish between “this test passes when the mocks are called as expected” from “I forgot to actually test for something”.

class SalutationTest << TestCase
  def test_that_we_log_statistics
    when_the_test_runs_then {
      Statistics.expects(:count).with('saluation.greeting.count')
    }

    # Given
    saluation = Salutation.new(Person.new('David','Copeland',:male))
    # When
    saluation.greeting
    # Then
    assert_mocks_were_called
  end

  def when_the_test_runs_then; yield; end
  def assert_mocks_were_called; end
end

We’ve still deviated from our canonical structure, but the test reads better: “When the test runs then expect this method to be called; now let’s run the test”

Of course, we’ve just taken our first step out of “plain old Ruby” and created framework code. This is the price you pay for using mocks; testing with mocks complicate our tests. By using some lightweight “control structure” helper methods, we can at least make the intent clear.

Block-Based Asserts Disrupt, too

There’s another pattern we see in tests that disrupts the structure in much the same way that the use of mocks does. That disruption is block-based asserts, the most common of which is assert_raises. For example, suppose we’re testing that our Saluation class requires a non-nil Person in its constructor. We could test that like so:

def test_that_constructor_requires_a_person
  assert_raises ArgumentError do
    Salutation.new(nil)
  end
end

This test is weird for two reasons: the first is that the “Given” is implicit. The second is that the “Then” comes before the “When”:

def test_that_constructor_requires_a_person
  # Given - we are going to use a nil Person
  # Then
  assert_raises ArgumentError do
    # When
    Salutation.new(nil)
  end
end

We can clean this up by creating a variable for our nil Person and putting our “Then” code inside a block, which we then pass to assert_raises:

def test_that_constructor_requires_a_person
  # Given
  nil_person = nil
  # When
  code = lambda { Salutation.new(nil_person) }
  # Then
  assert_raises(ArgumentError,&code)
end

We’ve had to jump through a slightly awkward hoop of putting the code-under-test in a lambda, but now things are in a consistent structure. This example might seem a bit too simplisitc. What about another popular block-based assertion, assert_difference? It’s commonly used in Rails apps to check that a certain number of records were written to the database. While I think that this assertion is generally not needed, it is commonly used.
Here’s an example where we suppose that an after_save hook is memoizing a derived field for us.

test "we can save and our after-save hook runs, generating the full_name attribute" do
  # Given
  first_name = 'David'
  last_name = 'Copeland'
 
  # Then
  assert_difference('Person.count') do
    # When
    person = Person.create(:first_name => first_name, last_name => last_name)
    # Then
    assert 'David Copeland',person.full_name
  end
end

Now the structure is very strange. If we try to apply our lambda solution above, it’s still a bit odd:

test "we can save and our after-save hook runs, generating the full_name attribute" do
  # Given
  first_name = 'David'
  last_name = 'Copeland'

  # When
  code = lambda {
    person = Person.create(:first_name => first_name, last_name => last_name)
    # Then
    assert 'David Copeland',person.full_name
  }
 
  # Then
  assert_difference('Person.count',&code)
end

Yikes. This is arguably worse. Since only one line of code inside our “When” block is really affecting the condition that assert_difference tests for, we can take advantage of Ruby’s ability to create instance variables on-demand and pass the person outside of the assert_difference block:

test "we can save and our after-save hook runs, generating the full_name attribute" do
  # Given
  first_name = 'David'
  last_name = 'Copeland'

  # When
  create_person = lambda { 
    @person = Person.create(:first_name => first_name, last_name => last_name)
  }
 
  # Then
  assert_difference('Person.count',&create_person)
  assert 'David Copeland',@person.full_name
end

That’s much better; we can now clearly see the setup, the code being tested, and all the assertions together.

It may seem slightly unusual, but by working to keep all your tests structured around Given/When/Then, you will find them readable weeks and months later, and others will be clearly able to see their intent.

Next

We still have a fair way to go to get our tests really clean and clear. For example, do we need to have those #Given, #When, and #Then comments everywhere? I think comments are powerful, but having the same group of comments everywhere feels like repetition we can eliminate. Another issue is the use of “magic values”, or literals, in our test code. In the test above, we create a male person with the name “David Copeland”. Is any of this relevant to the test? If not, why is it there?

We’ll deal with these issues in the next post.