Testing is a lesson in humility

I was working on a web site a few weeks ago and found that the auto_complete plugin didn’t work well when text fields were repeated on a form. Later I modified auto_complete to handle the case where text fields are repeated. I then refactored the code into a separate add-on plugin, but before I posted the code on github I decided to add a few tests just for the sake of completeness. I was confident that the text_field_with_auto_complete function I wrote last week was correct – after all, it was working inside my web site. I suppose you could call this method “Development Driven Testing:” write some code first, and then add a few tests after the fact to make yourself feel more confident that you have done your job correctly! Needless to say, I later realized this was a bad idea.

I wrote some simple test code similar to what Rails uses to test the FormHelper module: vendor/rails/actionpack/test/template/form_helper_test.rb. The idea behind my test was simple. As I explained last week, my code in text_field_with_auto_complete worked by insuring each generated <input> tag's id attribute was unique, and by using a name attribute taken from the surrounding call to fields_for, or form_for, instead of the standard name value “object[method].” (It also passes different values for the URL and “param_name” parameter into the Ajax code, but let’s skip that for now.) In other words, my code simply called into the original auto_complete plugin with some modified parameters. So to test this, I would just call the original and modified versions of text_field_with_auto_complete and compare the HTML after a search/replace for the desired changes. Here’s the test:

def test_auto_complete_fields_for_html
  standard_auto_complete_html =
    text_field_with_auto_complete :person,
                                  :name,
                                  {},
                                  { :param_name => 'person[name]' }
  _erbout = ''
  auto_complete_fields_for('group[person_attributes][]', @person) do |f|
    _erbout.concat f.text_field_with_auto_complete(:person, :name)
  end
  assert_dom_equal standard_auto_complete_html,
    _erbout.gsub(/group\[person_attributes\]\[\]/, 'person')
           .gsub(/person_[0-9]+_name/, 'person_name')
end

“Standard_auto_complete_html” contains the HTML the original auto_complete plugin generates. Then I call auto_complete_fields_for and my version of text_field_with_auto_complete, simulating a real ERB template, and write the HTML into _erbout. Finally we test that after search/replace on the expected changes the modified HTML is the same as the standard HTML. After the usual syntax errors and typos I ran the test again and completely expected it to work…

Of course, it failed! I couldn’t even discover what the error was until I wrote the HTML out on the console and took a close look at what was generated. Here’s the <input> tag the original auto_complete plugin generates:

<input id="person_name"
       name="person[name]"
       size="30"
       type="text"
       value="Someone Important" />

And here’s what my code generated before the search and replace:

<input id="person_15961340_name"
       name="group[person_attributes][][name]"
       size="30"
       type="text" />

Here we can see the expected changes to the id and name attributes, but the “value” attribute is missing from my HTML! I had no idea this was happening even when I used my plugin in a web site. I had noticed earlier that some values were missing in my site’s text fields in the browser but I had assumed this was normal behavior from auto_complete and simply added the values explicitly in the ERB.

Lesson learned: not only do tests insure your code works, but the process of writing the tests forces you to think much more deeply and carefully about what your code is really doing. Of course, once you start writing tests first the same thing applies to your design: you think much more carefully about what you are trying to do, and how to design a solution.

I went on to quickly fix the text_field_with_auto_complete function by passing in the original object and method parameters, so that Rails would get the proper value for me, and explicitly setting the id attribute with a unique number as follows:

@template.text_field_with_auto_complete(
      object,
      method,
      { :name => "#{@object_name}[#{method}]",
        :id => "#{object}_#{Object.new.object_id}_#{method}"
etc…

This got my test to pass! Relieved, I went on to write another test. This second test insures that the id attributes generated by the plugin are all unique:

def test_two_auto_complete_fields_have_different_ids
  id_attribute_pattern = /id=\"[^\"]*\"/i
  _erbout = ''
  _erbout2 = ''
  auto_complete_fields_for('group[person_attributes][]', @person) do |f|
    _erbout.concat f.text_field_with_auto_complete(:person, :name)
    _erbout2.concat f.text_field_with_auto_complete(:person, :name)
  end
  assert_equal
    [],
    _erbout.scan(id_attribute_pattern) & _erbout2.scan(id_attribute_pattern)
end

I call the text_field_with_auto_complete function twice and check that all of the <input> tags have unique id=”” attributes by scanning for the attributes and checking that the two arrays of matches have an empty intersection set. Sounds simple, right? Surely it will pass…

Getting surprised and humiliated by one unit test was bad enough… but the second test surprised me yet again! What happened here was that all of the <div> tags, also generated by text_field_with_auto_complete, had the same id attributes! I had written the test above to look for <input id=””> attributes but fortunately the code also matched <div id=””>, like this:

<div class="auto_complete" id="person_name_auto_complete"></div>

Since these id's were not unique, my test failed:

<[]> expected but was
<["id=\"person_name_auto_complete\""]>.
2 tests, 2 assertions, 1 failures, 0 errors

I finally solved the problem and got both of my tests to pass using this code:

def text_field_with_auto_complete(object, method,
                                  tag_options = {}, completion_options = {})
    object_value =
      ActionView::Helpers::InstanceTag.value_before_type_cast(@object,
                                                              method.to_s)
    @template.text_field_with_auto_complete(
      "#{object}_#{Object.new.object_id}",
      method,
      { :name => "#{@object_name}[#{method}]",
        :value => object_value
      }.update(tag_options),
      { :param_name => "#{object}[#{method}]",
        :url => { :action => "auto_complete_for_#{object}_#{method}" }
      }.update(completion_options)
    )
  end

To completely understand why the value attribute was missing and how to get it back I took a look at how the FormHelper module in Rails worked. A long story short: I get the object’s value by carefully using the same code that the FormHelper module does by calling InstanceTag.value_before_type_cast, and then pass the value in as a parameter to text_field_with_auto_complete. I was sure to obtain the proper object’s value by using “@object” from the FormBuilder base class. And now the <div id="">'s are unique since we pass in the modified object name into the original text_field_with_auto_complete.