Declare or Impair

A topic that crops up often on the Cucumber mailing list is whether it's best to write BDD tests in imperative or declarative style. Back in 2008, a blog post by Ben Mabey described some weaknesses of the imperative style, especially its tendency toward brittleness. Mike Swieton echoes this argument in Never say "Click", while others such as Zach Moazeni disagree, preferring to keep steps as generic and reusable as possible.

I won't rehash the examples and arguments for both approaches here; the links above explain it better than I could, anyway. I would like to explore a little about why the distinction is important, and why programmers and automated testers should know the difference.

When I was starting out with BDD, I didn't think much about imperative vs. declarative test scenarios. Programmers who have only worked with imperative programming languages don't often consider that a declarative programming approach is possible. The first time I heard the term "imperative" in the context of computer programming, it confused me a little, because I had only done imperative programming and didn't realize there was any other way to program. As they say, fish don't know they're wet.

At the lowest levels, computer languages are imperative. Assembly languages are about as imperative as you can get; assembler pseudocode might read something like this:

Load the value at address X into register A
Load the value at address Y into register B
Add register B to register A
Store the contents of register A to address Z

Imperative programs are all about how to do it. They are, by necessity, long and detailed, like the multilingual instructions that come with an unassembled furniture kit. Insert tab A into slot B, insert screw C into hole D, tighten with screwdriver.

The higher-level a programming language is, the more declarative it tends to be. The most common operations such as memory-address-tracking, register-loading, loop-variable-incrementing and other complexities are wrapped up in convenient abstractions like variables, expressions, and iteration. Rather than give the machine step-by-step instructions on how to load X and Y into registers A and B, add them together, and store the result in memory location Z, we just say:

Z = X + Y

We declare Z's relationship to X and Y, instead of explicitly describing how to calculate Z. Yes, technically this is still an imperative statement (unless it's a functional language, in which case you're declaring a function called Z that returns the sum of functions X and Y). The point is, we've taken a big step away from the low-level details, toward a more natural, condensed expression that is less mentally taxing. It reads more like a statement of fact than a command to do.

Declarative programming is the what, rather than the how. Declarative instruction is, if I may stretch the analogy, more like the illustrations that depict the spatial relationships between the unassembled furniture pieces, instead of trying to explain all of the steps using words alone. The higher-level pictures tend to be more intuitive and easier to follow than the verbose, low-level imperative instructions. I don't know about you, but I usually just follow the pictures when I'm assembling furniture, or Lego models. And I certainly prefer writing Python, Haskell, or Ruby to writing Java, C or assembler.

How does this translate to writing business-oriented test scenarios? Well, as the articles linked above demonstrate, and as is often mentioned in places like the Cucumber mailing list, a person familiar with the test domain can usually get by with:

Given I am logged into the website

and doesn't need:

When I go to the login page
And I fill in the username with "admin"
And I fill in the password with "spork"
And I press "Login"

Such low-level minutiae can be a major distraction to someone reading (or writing) test cases. True, occasionally those low-level details are important, like when you're thoroughly testing the various ways that the login process can succeed or fail. But when the login process is not what you're testing, there's no reason to bore the viewer (or author) with the mundane stuff.

Of course, at some point, any declarative steps you write in a BDD scenario need to be translated into imperative directions for the computer to follow; that's pretty much inevitable (and is one of the reasons I created kelp). The beauty of Cucumber is that you get to define the language the tests are written in. The imperative details can often be hidden away in the step definitions, where they can be maintained by the programmers who need to concern themselves with such things. Ideally, the test scenarios themselves should be comprehensible to the business domain experts, and to the average user.

Programmers may find it hard to break out of the imperative frame of mind, but declarative writing can lead to more compact, intelligible tests that need less maintenance in the long run.