Test Behavior, not Configuration

May 23, 2016

Join my mailing list…

I’ve become re-acquainted with the pattern of testing ActiveRecord classes using stuff like expect(parent).to belong_to(:child) and I just don’t understand why anyone would ever write a test like that. It provides no value, and the implementation provided by shoulda isn’t actually testing the behavior. It’s testing configuration.

In Rails, the following code is configuration:

class Address < ActiveRecord::Base
  belongs_to :country
end

class Country < ActiveRecord::Base
end

There is literally no reason to write this test:

describe Address do
  it "belongs to a country" do
    expect(Address.new).to belong_to(:country)
  end
end

Why?

First, it’s basically asserting the exact code that it’s testing. It could just as well be:

describe Address do
  it "belongs to a country" do
    source_code = File.read(File.join(
                              __FILE__,
                              "../../app/models/address.rb"))
    expect(source_code).to =~(/^  belongs_to :country$/)
  end
end

Second, it actually doesn’t test the configuration behavior. It uses a ridiculous amount of logic and meta-programming to determine what Active Record methods appear in the class under test.

It does not assert any particular behavior.

This means that it actually doesn’t test the one thing that most people mess up with ActiveRecord, which is putting the belongs_to on the wrong class.

It’s also highly unlikely that this test would ever find a real bug, and I can’t imagine a TDD scenario in which this test takes our code from red to green.

But, since it doesn’t test behavior it makes refactoring difficult.

What if we tested the behavior instead?

Here’s one way to do that:

describe Address do
  it "has a country" do
    address = Address.new
    country = Country.new
    address.country = country

    expect(address.country).to eq(country)
  end
end

This is how we expect Address instances to behave. We want to give them countries, and have them return them to us. It’s almost certain that most uses of an address and a country will do it this way.

Now, suppose we’ve decided that storing countries in its own table is too difficult, becuase the geo-political situtation on our planet is chaotic. Instead, we’ll store it as a string on addresses called country_code. This way, when countries change, we don’t have to maintain our countries list.

class Address < ActiveRecord::Base

  belongs_to :legacy_country, foreign_key :country_id

  def country
    Country.new(code: self.country_code)
  end

  def country=(new_country)
    self.country_code = new_country.code
  end
end

class Country < ActiveRecord::Base
  def ==(other_country)
    self.code == other_country.code
  end
end

With this change, the behavior of our Address stays the same, and our test still passes. If we had asserted the configuration instead, our test would break, even though the behavior was the same.

Don’t test configuration. Test behavior.