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:

With this structure you know every possible state is tested.

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:

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:

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

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:

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:

Their solution is concise, easy to understand and easy to create. It’s also actual ruby code making it easier 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.

Accessory gems, part II: Local Gemfiles

The Bundler Ruby gem does its job well: it ensures that your application sees only the gems listed in its Gemfile. Sometimes, though, you might feel like it’s doing its job too well. You might want to add a gem to your app temporarily, perhaps for monitoring or debugging, or your app might have a gem in its Gemfile that isn’t needed in every environment and that you don’t want in yours. Bundler just says no. If a gem is in your app’s Gemfile your app will see it; if not, it won’t. If you want to add a gem to or remove it from your app, you have to add it to or remove it from your Gemfile and

.

Just editing your app’s Gemfile when you need to add or remove a gem has a couple of drawbacks:

  • It’s cumbersome. If you need to make the same change to your Gemfile regularly, that will get old fast. Besides, doing things manually and repeatedly is not what programming is all about.
  • It dirties your version control. You should be able to commit quickly and effortlessly, without having to pick through modified files to exclude changes not meant for production. And the fewer chances you have to mistakenly commit a change that you meant to be local-only, the better off we all are.

How can we give our Gemfiles some flexibility without those hassles? Bundler gives us a couple of openings: we can use a different name for our customized Gemfile to keep it out of the way of version control, and, since Gemfiles are just Ruby scripts, we can write code in our customized Gemfile to reuse our base Gemfile without copying and modifying it.

First, let’s give our customized Gemfile a new name. In part I, when we solved a related problem involving Gemfiles, we learned that we can tell Bundler to look for an app’s Gemfile under a different name by setting the environment variable

. If we name our local Gemfile

and set

to that, Bundler will use

for the lock file name. We can tell our version control to ignore those files (e.g. for git, add them to our

) and never have to think about them again when committing. So

is a good way to have a customized Gemfile without version control hassles.

If you usually work on a single project, you can set

in your shell environment and forget about it. You might also want to configure your editor to set

before running your app or its tests, for example in RubyMine’s run/debug configuration defaults. If you work on some projects that do have a Gemfile.local and some that don’t, you can set

on the command line, as in the following example:

You could also set and unset

with direnv, or do it in scripts or shell aliases.

Second, let’s see how we can reuse one Gemfile in another. Here’s a

that includes a base Gemfile and then adds and removes gems.

is a method in Bundler’s DSL. It reads the file with the given name,

s its contents and handles errors the way that Bundler does elsewhere. It’s an internal method, so it might go away; if it does,

will work pretty much the same way.

Adding gems is simple: just use the

method as you would in a standalone Gemfile.

Removing gems is a little hackier. Peeking at Bundler’s DSL again, we see that the

method accumulates dependencies in the

array. To forget a gem that was in the base Gemfile, just delete it from that array. (This example deletes the debugger gem, which is in my app’s Gemfile for the convenience of our CLI diehards but breaks RubyMine’s debugger.)

That covers everything I’ve needed to do in my Gemfile.local.

Finally, let’s solve a problem that we just introduced: how to preserve all of the dependencies from the base Gemfile. If we just

, Bundler will build a new

with a potentially different set of dependencies than those in

. Fortunately, if we copy

to

before we

, Bundler does exactly what we need: it reads the versions from

, adds and removes the gems that we added and removed in

, and writes a new

!  To keep up with updates to the base

and

, each time those files change we need to

the base Gemfile, copy

to

, and then

our

. That’s a bit of a pain, but it’s not so bad with a shell alias:

Whenever you check out a new version of the base Gemfile and Bundler complains that a gem is missing, just run

instead of

like you would ordinarily do. (This example assumes you’ve set

in your shell and need to set it back to the default to

.) Alternatively, you could bundle, copy and bundle in a git hook.

Thanks to Ruby and Bundler’s open design, your app’s Gemfile isn’t the straitjacket that it first appears to be. With a little configuration you can keep your application’s carefully chosen and tested dependencies and still customize it to meet the needs of particular environments.