Feeling like carrying bags of sand

A dreary afternoon

A while ago I was in the office, while the team was busy programming. I heard one of the developers typing with some intensity, and puffing. “What are you working on?” — I asked. — “I’m working on this administration panel. And it’s darned tiresome work!!”

So I started observing him. The feature he was programming was a very simple administration screen: clicking on a certain link should open a page with the details of a given user: name, email and so on. Nothing particularly complicated. Why the difficulty then? Poul (not his real name) was developing the feature incrementally, with TDD. He started with a test like

it "displays information for a given user" do
  Factory.create(:user, :id => "1234", :first_name => "Arthur")
  get "/users/display?id=1234"
  
  assert_select "table" do
    assert_select "td#user_first_name", "Arthur"
  end
end

This test basically means: let’s save a user in the database. When I open the page at the given url, it will show a table that contains a cell with id “user_first_name” and contents “Arthur”. So far so good; getting this test to pass was not complicated. The test does not specify very precisely how the page should look; as long as we have a table with the proper data in it. A bit of a sore point is that in order to find the proper information, an artificial html id attribute had to be created.

<table>
  <tr class="even">
    <td><strong>First Name</strong></td>
    <td id="user_first_name"><%= @user.first_name %></td>
  </tr>
</table>

Then Poul went on refining this work. He updated the test

it "displays information for a given user" do
  Factory.create(:user, :id => "1234", 
    :first_name => "Arthur", :last_name => "Fonzarelli")
  get "/users/display?id=1234"
  
  assert_select "table" do
    assert_select "td#user_first_name", "Arthur"
    assert_select "td#user_last_name", "Fonzarelli"
  end
end

Verified that the test failed. Updated the page template.

<table>
  <tr class="even">
    <td><strong>First Name</strong></td>
    <td id="user_first_name"><%= @user.first_name %></td>
  </tr>
  <tr class="odd">
    <td><strong>Last Name</strong></td>
    <td id="user_last_name"><%= @user.last_name %></td>
  </tr>
</table>

Verified that the test was now passing. And again update the test:

it "displays information for a given user" do
  Factory.create(:user, :id => "1234", 
    :first_name => "Arthur", :last_name => "Fonzarelli", 
    :email => "fonzie@happydays.com")
  get "/users/display?id=1234"
  
  assert_select "table" do
    assert_select "td#user_first_name", "Arthur"
    assert_select "td#user_last_name", "Fonzarelli"
    assert_select "td#user_email", "fonzie@happydays.com"
  end
end

Run the tests. Red. Update the template. Run the tests. Green. And so on.

Can you see how Poul was getting tired of this? It sounds, and feels, like work. Like hard work. Like boring, dreary, tiring work. Like carrying sacks of sand from point A to point B.

Wait. TDD should not feel this way. Programming should not feel this way. Programming is about the joy of seeing the computer doing things. TDD is about the geek joy of developing elegant code that works. About programming with ease. About working smart!

I thank Mike Hill for the expression “geek joy”. It’s a sort of compass I can use to check that what I’m doing, and the team is doing, is right. Right now something is amiss. We are programming in a way that is not joyful, and is slow, and is boring. What’s happening? Take 5 minutes to analyze the situation and find your own answer. Then I’ll give you mine.


*              *
*

What’s wrong here?

There are other indicators besides the feelings of dreariness that point us in the direction of the problem. The main indicator for me is duplication. Consider

<table>
  <tr class="even">
    <td><strong>First Name</strong></td>
    <td id="user_first_name"><%= @user.first_name %></td>
  </tr>
  <tr class="odd">
    <td><strong>Last Name</strong></td>
    <td id="user_last_name"><%= @user.last_name %></td>
  </tr>
  <tr class="even">
    <td><strong>Email</strong></td>
    <td id="user_email"><%= @user.email %></td>
  </tr>
</table>

and tell me if you don’t see the duplication? It bursts with duplication! The original definition of TDD says:

  1. Quickly add a test.
  2. Run all tests and see the new one fail.
  3. Make a little change.
  4. Run all tests and see them all succeed.
  5. Refactor to remove duplication.

Kent Beck, Test Driven Development: By Example

Repeat with me: Refactor to remove duplication. Not “refactor at will” nor “refactor until I like it”. It’s “Refactor to remove duplication“. If I’m doing TDD, then I should not start the next test until I removed all the duplication I can see.

Other considerations:

  • The feature is tested with an integration test; we are testing both the query (it should find the user with the given ID) and the view rendering. This is not a unit test. This is a broad test. Part of the dreariness comes from the fact that the test is slow, and it’s slow because it tests too much.
  • The view is not tested in a particularly stringent way. The “even”, “odd” sequence, for instance, might be wrong or missing without breaking the test. This sort of test cannot afford to be too precise about how the html code is generated, otherwise it would be too brittle.
  • No new objects are appearing. This test is just adding stuff to the BackofficeUsersController, and adding a new view template. None of this code will ever be reused anywhere. The work we are doing here is not improving the design of the system as a whole. For all the care we are putting in testing it.
  • The feature is being developed with a single test. The increments that Poul is producing are adding detail to the existing test, instead of writing new tests that specify new interesting behaviour. This is a key insight.

What to do?

Just remove duplication. Start with

<table>
  <%= admin_table_row "even", "First Name", @user.first_name %>
  <%= admin_table_row "odd", "Last Name", @user.last_name %>
  <%= admin_table_row "even", "Email", @user.email %>
</table>

proceed with

<%= 
  AdminTable.new(@user.attributes_for_administration).to_html
%>

Now here’s something. If we create an AdminTable object, we can have fun test-driving it like this:

it "produces an html table" do
  model = [["Label 0", "value 0"]]
  expected = <<-EOF
    <table>
      <tr class="even">
        <td><strong>Label 0</strong></td>
        <td>value 0</td>
      </tr>
    </table>
  EOF
  
  assert_dom_equal expected, AdminTable.new(model).to_html
end

This test can afford to test precisely the HTML we produce. This will be the *only place* where we test this html, so it should be the only test that breaks when we change the details of how an AdminTable is rendered in HTML. At the same time, we can use this AdminTable for all similar tables in administration pages, speeding up future development.

Note also that there is no need to invent id attributes for the cells.

This test is fun to write. It’s completely decoupled from the details of our User model, from ActiveRecord, from Rails. The time spent writing this test will be well spent.

All that remains to test-drive is the invocation

@user.attributes_for_administration

We managed to separate the view from the business rule. This again is a simple and (relatively) fun test to write. All it has to do is report a list of values and a list of labels. The dreariness in the development of this code:

def attributes_for_administration
  [
    ["First Name", self.first_name],
    ["Last Name", self.last_name],
    ...
  ]
end

hints that there should be some other place for specifying the human-readable labels for the columns. This calls for more design work, and more improvement of the codebase. And above all, more fun!

Conclusion

I learned TDD for the sheer awesomeness of it. The coolness! Writing clean code that works; working with ease. Being more effective. We should all be on the lookout for the signs that we’re working on autopilot, that we’re not having fun, that we’re not writing small and powerful code that does a lot of work.

Have fun!

One Response to “Feeling like carrying bags of sand”

  1. Stefano Verna Says:

    Ho apprezzato un sacco questo post. Sono ai miei primi esperimenti seri con il mondo dei test e la prospettiva di cercare di testare cose “divertenti” e “generalizzanti” piuttosto che cose noiose e fini a se’ stesse รจ un qualcosa che ancora mi mancava. Grazie!

Leave a Reply