Making the Most of BDD, Part 2

Hi, I’m Mary-Anne. I’m a senior Ruby developer here at Envato. One of the things that I love about my job is that it gives me the opportunity to use one of the practices I am most passionate about - Behaviour Driven Development, or BDD. In Part 1 of this 2-part series I described what BDD is and explained how it is more than simply a way to improve code quality. Today, let’s look at how BDD becomes the living documentation of your system, and how it informs your system architecture.

A Living Document

Living cucumbers An invaluable benefit of having well written functional and unit test suites is that they are the only form of documentation for the system under test that is guaranteed to be correct. When you are starting work on a complex system for the first time the tests are often the best way of learning how the system works.

For this reason it is critical to pay attention to writing your tests expressively. Make sure that the important information is clearly visible, and unnecessary details are hidden away.

In this example, we create helper methods with parameters that showcase the important variables. mock_default_account creates a mock payment account with a given payment service (PayPal or Skrill). any_valid_params is a set of parameters that works for the form. We are only interested in two of these for the test, so we merge in the two we really care about. We place these helper methods out of the way at the bottom of the RSpec block, so we don’t have to read them unless we want the details.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
require 'rails_helper'

RSpec.describe Withdrawal::RequestForm, :type => :model do
  subject(:form) { Withdrawal::RequestForm.new(user, params, request_forensics) }
  let(:user) { User.make!(:earnings_balance => Money.new(800_00)) }
  before do
    allow(Withdrawal::DefaultWithdrawalAccount).to receive(:default_for_user).with(user).and_return(default_account)
  end

  describe '#save' do

    context 'successful save' do
      context 'and the author has chosen to use their default account' do
        let(:default_account) { mock_default_account('paypal') }
        let(:params) { any_valid_params.merge(:use_default_account => true, :service => 'skrill') }

        it 'creates a withdrawal based on the default account' do
          expect(form.save).to be true
          expect(Withdrawal.last.payment_service).to eq('paypal')
        end
      end

      context 'and the author has not chosen to use their default account' do
        let(:default_account) {  mock_default_account('paypal') }
        let(:params) { any_valid_params.merge(:use_default_account => false, :service => 'skrill') }

        it 'creates a withdrawal based on the form parameters entered by the user' do
          expect(form.save).to be true
          expect(Withdrawal.last.payment_service).to eq('skrill')
        end
      end
    end
  end

  def any_valid_params
    {
      :type                                 => 'single_fixed',
      :amount                               => '500',
      :service                              => 'paypal',
      :paypal_email_address                 => 'test@envato.com',
      :paypal_email_address_confirmation    => 'test@envato.com',
      :skrill_email_address                 => 'test2@envato.com',
      :skrill_email_address_confirmation    => 'test2@envato.com',
      :taxable_australian_resident          => true,
      :hobbyist                             => true
    }
  end

  def mock_default_account(payment_service)
    mock_model(Withdrawal::DefaultWithdrawalAccount, :user => user, :payment_service => payment_service, :payment_email_address => 'me@here.com', :swift_detail => nil)
  end

end

Good RSpec tests form technical documentation that provide a detailed view of how your system works. In contrast, good Cucumber Scenarios are a window on the business value of your application. Every value that appears in your test, whether it’s an RSpec or a Scenario, should be important, and it should be clear where it came from and what has happened to it. Here’s a Scenario that is giving more detail than someone who’s looking for an overview on the functionality will want:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  Scenario: Request a single withdrawal with a fixed amount
    Given I have earned $1000
    When I go to the withdrawal page
    And I fill in "Full Name" with "Alan Somebody"
    And I fill in "Withdrawal Amount" with "100"
    And I select "PayPal" from "Payment Service"
    And I fill in "Email Address" with "alan@somewhere.com"
    And I fill in "Billing Address Line 1" with "Somewhere"
    And I fill in "Billing Address Line 2" with "Melbourne"
    And I fill in "Billing Address Line 3" with "Victoria"
    And I fill in "City" with "City"
    And I fill in "State" with "Here"
    And I fill in "Postcode" with "1234"
    And I submit the withdrawal request
    Then I should see "Your withdrawal request has been sent."
    And I should see my balance as $900
    And I should have a withdrawal with the following details:
      | amount                | 100.00             |
      | payment_email_address | alan@somewhere.com |
      | maximum_at_period_end | false              |

In our first refactoring attempt, we hide the detail behind a single step:

1
2
3
4
5
6
7
8
9
10
11
  Scenario: Request a single withdrawal with a fixed amount
    Given I have earned $1000
    When I go to the withdrawal page
    And I fill the form 
    And I submit the withdrawal request
    Then I should see "Your withdrawal request has been sent."
    And I should see my balance as $900
    And I should have a withdrawal with the following details:
      | amount                | 100.00             |
      | payment_email_address | alan@somewhere.com |
      | maximum_at_period_end | false              |

Now the $900 balance and the 100.00 withdrawal amount appear as if by magic in the assertions. The reader has to dive into the steps to figure out where those values came from. Let’s refactor that so that the important values are called out:

1
2
3
4
5
6
7
8
9
10
11
12
  Scenario: Request a single withdrawal with a fixed amount
    Given I have earned $1000
    When I go to the withdrawal page
    And I fill the form to request a single $100 withdrawal
    And I select to withdraw via PayPal with the email "alan@somewhere.com"
    And I submit the withdrawal request
    Then I should see "Your withdrawal request has been sent."
    And I should see my balance as $900
    And I should have a withdrawal with the following details:
      | amount                | 100.00             |
      | payment_email_address | alan@somewhere.com |
      | maximum_at_period_end | false              |

Tips

  • Make the interesting data the hero. Hide the boring stuff.
  • Don’t be afraid to use literal strings. They catch the eye.
  • Avoid magic values
  • Describe … is what you are testing. Describe can be:
    • an instance method name (#my_method)
    • a class method name (.my_method)
    • an English description
  • Context … is the environment it’s running in.
  • Use subject {…} and it {…} for succinctness.
  • Do not use should. In code, use expect(...) instead. In descriptions, describe what it does, not what it should do.

A Sound Architecture

Humpty Dumpty sat on a wall. It was a pretty solid wall... When you write tests before code, you are by definition writing code that is testable. Testable code is more cohesive and less coupled, and that will benefit your architecture.

Moreover, when your application has close to 100% test coverage, you can feel confident to refactor code, knowing that if you break anything your tests will probably catch it. Without the freedom to refactor, the application architecture becomes brittle over time. Eventually your app will be unmaintainable.

Ideally your unit tests will have a sharp focus on the class that is being tested. Classes rarely operate in isolation, however. What is the best way of creating the dependencies of the class that’s under test? There are two main ways to handle this problem: test doubles and object factories. Each approach has its pros and cons.

If you are working on a new system, introducing a test object creation factory like Machinist or Factory Girl can detract from the architectural benefits of TDD by making it just too easy to create complex object structures. This can allow your models to quickly get out of hand.

If, however, you are working in a system that already has a complex model structure, object factories can be invaluable for helping you write very readable tests very quickly. This is a trade-off you need to be pragmatic about. Use test doubles when the setup is not too difficult. Use object factories when necessary.

In Rails applications, there are many different kinds of test doubles you can use. Here are some examples:

Double

A simple double is fine when you need a parameter and don’t particularly care about its behaviour. eg let(:presenter) { double }

Instance Double

Create an instance double by passing in a class. When you specify its behaviour any methods that would not be available on an object of that class will cause a test failure. This helps ensure your tests are realistic. eg let(:item_variant) { instance_double(ItemVariant, :cost => Money.new(800)) }

Mock Model

Use mock_model if you need a double for an active record class. mock_model automatically supplies active record methods like #id. You can also add .as_null_object() to avoid having to specify all internal method calls. Any methods that are called on the mock object that have not had a return value specified for them will return the mock (not null as you might expect!). eg: let(:item) { mock_model(Item).as_null_object }

Stubbing methods

You can replace the behaviour of a single method on either a real object or a mock object by using allow(x).to receive(:y).and_return(z). (These replace the older syntax x.stub, x.should_receive and x.should_not_receive.) eg: 
allow(Buying::DepositAndCheckout::FormPresenter).to receive(:new).and_return(presenter)

Stubbing method chains

allow(x).to receive_method_chain(:a, :b, :c …).and_return(q) is a simple way of replacing the behaviour of a single method on a deeply nested dependency. (This is the new syntax to replace stub_chain().) eg: 
allow(Collection).to receive_message_chain(:not_favorite, :find).and_return(collection)

Tips:

  • Use a simple double when you don’t really care about the behaviour of the dependency
  • Use an instance double when the dependency’s behaviour is important and you want to be sure your test setup is correct
  • Use method stubs to:
  • verify the calling behaviour of the class under test
  • avoid interactions with dependencies, even if they are not injected
  • control the behaviour of the class under test to allow you to test edge cases easily
  • Use allow().to receive_method_chain() to avoid having to create deeply nested dependencies
  • Use manufactured objects when attempting to use a double results in a test that is overly complex and difficult to read
  • Use manufactured objects for integration tests that test the interaction across classes

A Refactoring

Here is an example refactoring putting into practice some of the things I’ve talked about.

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
describe '#recommended_items_for_item' do
  let(:item) { double(:item, :id => 7917671) }
  let(:response) do
    {
      "class"      => ["recommendations", "collection"],
      "properties" => {"facts" => {"item_id" => "7917671", "Site" => "themeforest.net", "category" => "wordpress"},
                       "skipped"  => [],
                      "included" => ["item-items"]},
      "entities"   => [
        {
          "class"      => [
            "grouping"
          ],
          "rel"        => "http://www.envato.com/rels/recommended/recommendation-grouping",
          "properties" => {...},
          "entities"   => [
            {
              "class"      => [
                "item",
                "recommendation"
              ],
              "rel"        => [
                "http://www.envato.com/rels/recommended/marketplace/item"
              ],
              "properties" => {
                "id"          => 7668951,
                "recommender" => "item-items"
              }
            }
          ]
        }
      ]
    }
  end

  let(:expected_url) { "http://localhost/recommender_api/get_recommendations?category=wordpress&item_id=7917671&restrictions=item-items&site=themeforest.net" }
  let(:options) { {:timeout => 1} }

  it 'calls the item item recommender' do
    item.stub_chain(:category, :root, :site, :domain).and_return("themeforest.net")
    item.stub_chain(:category, :root, :path).and_return("wordpress")
    expect(RecommenderApi::Recommender).to receive(:get_recommendations).and_return(response)

    result = RecommenderApiAdapter.recommended_items_for_item(item)

    expect(result).to eq([{
                            :title=>"Suggested items",
                            :url=>"http://themeforest.net",
                            :ids=>[RecommenderApiAdapter::Suggestion.new(7668951)]
                          }])
  end
end

After:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
describe '#recommended_items_for_item' do
  let(:item) { mock_model(Item).as_null_object }
  let(:response) do
    {
      "class"      => ["recommendations", "collection"],
      "entities"   => [{"class" => ["grouping"],
                        "properties" => {
                          "name" => "Suggested items",
                          "href" => "http://themeforest.net"
                        },
                        "entities"   => [{"class" => ["item", "recommendation"], "properties" => {"id" => 7668951, }}]
                       }]
    }
  end

  it 'returns the recommended items as suggestions' do
    expect(RecommenderApi::Recommender).to receive(:get_recommendations).and_return(response)

    result = RecommenderApiAdapter.recommended_items_for_item(item)

    expect(result).to eq([{
                            :title=>"Suggested items",
                            :url=>"http://themeforest.net",
                            :ids=>[RecommenderApiAdapter::Suggestion.new(7668951)]
                          }])
  end
end

Be Pragmatic

I’d like to conclude with one final piece of advice. In everything you do in BDD and TDD, be pragmatic. David Heinemeier Hansson, in his controversial ‘TDD is Dead’ speech, made the flawed assumption that all TDD practices must be followed religiously for the practice to be called TDD.

In reality, there are many times when you should feel free to be pragmatic. If it’s hard to create a mock object, feel free to use a real object. If it’s hard to create a real object, feel free to use a factory like Machinist or FactoryGirl. Don’t feel that your unit tests always have to be strictly isolated to the class under test - though that should be your aim most of the time.

You don’t always have to write a test before you change code. If you don’t have a clear picture in your mind of the change you need to make, then give yourself the freedom to do some code experimentation before you write your tests. If you are changing a configuration value, eg a constant, do not write a test that ensures it has that value. A test that is a mirror of the code is no help; it just adds to maintenance burden.

Being pragmatic doesn’t mean you’re not doing TDD. You can still write your tests first most of the time, and take advantage of all the great benefits that TDD brings.

Want more?

Check out the Prezi