ASCII Tables For Clearer Testing

I often find it difficult to understand a test. This can happen when the test was written by someone else, or even when I wrote it myself mere months ago. Usually the heart of the test is simple and each line of code is easily understood, but the context and setup can be extensive and located far away.

I’ve started using ASCII tables to define the initial test data. These tables are both concise and easy to understand, and that leads to simpler tests that are easier to maintain.

For context, at Indiegogo we use rspec, jasmine, and other testing frameworks in the Behavior-Driven Development category. Our tests will often specify nested contexts that enumerate possible states before adding our expectations. A typical test looks something like this:


describe '#my_method' do
  context 'when foo is true' do
    let(:foo) { true }
    context 'and bar is true' do
      let (:bar) { true }
      it 'returns W' do
        expect(my_obj.my_method).to eq('W')
      end
    end
    context 'and bar is false' do
      let(:bar) { false }
      it 'returns X' do
        expect(my_obj.my_method).to eq('X')
      end
    end
  end
  context 'when foo is false' do
    let (:foo) { false }
    context 'and bar is true' do
      let (:bar) { true }
      it 'returns Y' do
        expect(my_obj.my_method).to eq('Y')
      end
    end
    context 'and bar is false' do
      let (:bar) { false }
      it 'returns Z' do
        expect(my_obj.my_method).to eq('Z')
      end
    end
  end
end

Unfortunately, these structures often explode into huge files with thousands of lines of code. It’s pretty difficult to know the expected state for a line of code when the test data is initialized in multiple places hundreds of lines away.

Now, consider an alternative organization for the example above using ASCII tables:


|-------+-------+----------|
| foo   | bar   | expected |
|-------+-------+----------|
| true  | true  | W        |
|-------+-------+----------|
| true  | false | X        |
|-------+-------+----------|
| false | true  | Y        |
|-------+-------+----------|
| false | false | Z        |
|-------+-------+----------|

The table provides a concise and easily-understood presentation of the various states and the expected result.

This is not a new idea. Ward Cunningham’s Fit: Framework for Integrated Test and Bob Martin’s FitNesse both show state and expectations in tables, but making tables with ASCII characters means that you can embed these in existing test files, yielding many of the benefits of Fit and FitNesse without adopting the whole framework.

Introducing The ATV Gem

We initially added tables as here documents to some spec files and parsed them with locally-defined methods. Now we have extracted these table-parsing methods and created an open source project on GitHub, as well as the ATV (ASCII Table Values) ruby gem.

Building Tests With ATV

ATV returns your data as strings. Your code can use those strings however you want:

You can insert dynamic values using string interpolation:


data = <<EOD
|------+---------------|
| foo  | date          |
|------+---------------|
| true | #{Date.today} |
|------+---------------|
EOD

With a little meta-programming you can include methods that are rspec assertions:


describe '#new?' do
  cases_as_table = <<TEXT
|--------------------------+-----------|
| email                    | assertion |
|--------------------------+-----------|
| neighbors@example.com    | to        |
|--------------------------+-----------|
| message_8302@example.com | not_to    |
|--------------------------+-----------|
TEXT
  it 'handles these cases' do
    ATV.from_string(cases_as_table).each do |row|
      attributes = row.to_hash
      assertion = attributes.delete('assertion').to_sym

      my_obj = MyClass.new(attributes)
      expect(my_obj).send(assertion, be_new, '#{assertion} be_new failed for #{attributes.inspect}')
      # for example:
      # expect(my_obj).to be_new, 'to be_new failed for {'email'=&gt;'neighbors@example.com'}
    end
  end
end

One challenge with expressing rspec assertions in tables is keeping the rspec failure message meaningful. Normally that message is generated from the describe, contexts and example descriptions.

A possible remedy uses the table to dynamically create the complete rspec example, including the description. Another approach is to add a failure message to your rspec assertion, as we did above.

Here a method is used to load the data (via ATV) and assign the resulting instances to instance variables so that specific, individual object instances can be accessed in the example:


def read_and_assign_from_ascii_table(ascii_table)
  ATV.from_string(ascii_table).each do |row|
    attributes = row.to_hash
    name = attributes.delete('name')
    instance_variable_set(name.to_sym, MyClass.new(attributes))
  end
end

it 'handles these cases' do
  states_as_table = <<TEXT
|------+-------|
| name | state |
|------+-------|
| @new | new   |
|------+-------|
| @old | old   |
|------+-------|
TEXT
  read_and_assign_from_ascii_table(states_as_table)
  expect(@new).to be_new
  expect(@old).not_to be_new
end

Tables That Are Not ASCII Tables

Some of my coworkers were inspired by the tables concept but wanted something different. Their approach uses white space to organize a hash for easy reading:


header =
  {:whn =>   [:foo , :bar ], :expt => [:meth]}

examples =
  [
    {:whn => [true , true ], :expt => ['W'  ]},
    {:whn => [true , false], :expt => ['X'  ]},
    {:whn => [false, true ], :expt => ['Y'  ]},
    {:whn => [false, false], :expt => ['Z'  ]}
  ]

Their solution is concise, easy to understand and easy to create. It’s also actual ruby code making it easier to customize. Obviously ASCII tables are not the only way to organize your data.

How do you create these ASCII tables?

The benefit of these tables is easy to see but they may be difficult to create. I suspect most modern editors have modes that ease the creation and maintenance of these tables. I prefer to use the built-in table editor that comes with emacs Org Mode, or you can use the terminal-table gem to create your tables.

RubyMine’s column selection mode helps but I’m hoping this offering will inspire you to create a RubyMine plugin that is column aware like the table editor in org mode. If you do, please share.

Are ASCII tables right for your test?

Scale is important when deciding if you should use an ASCII table or some other approach for establishing test data. An ASCII table is probably overkill if you are initializing just a few variables. Likewise, if your test is initializing 2 or more attributes with two or more states then consider what the initialization would be like if summarized in a table. Also consider organizing your data using arrays and hashes with white space to show structure.