|Procedural tests focus on a series of steps; declarative tests work from the input and states. Procedural tests are natural to write, but consider whether a declarative test expresses your intent more concisely and clearly.|
There are two common styles of writing tests. One style might be called procedural: a test consists of a series of steps, each acting on or testing the state of the system. The second style might be called declarative: given a state of the system and some inputs, test the resulting state and the outputs. (These terms are from the theory of programming languages.) If we were using Ward Cunningham's fit testing framework, we might think of these as prototypically ActionFixtures vs. ColumnFixtures.
Let's look at a small, concrete example: a simple login screen.
We'd like to test that the OK button is highlighted only if both a username and a password have been specified.
Here's a test in the procedural style:
Here's a test of the same capability in a declarative style:
There's a sense in which the first test is more natural to write: it tells you what to do, step by step. But consider some advantages of the second style:
- If we want to know whether a particular test case is covered, the second style shows it more directly. For example, what if the username and password are both empty? It's easy to see that this case has been considered in the second test.
- The declarative style fails or passes one row at a time. The procedural style is vulnerable, in that if a check in the middle fails, the whole rest of the test may be invalid. (This isn't "free" though – the code connecting the test to the system must be designed to make this test independence work.)
- The declarative style has all the critical state, inputs, and outputs listed explicitly. In the procedural style, you may have to trace the state through many steps.
- The procedural style tends to presume it knows details about the interface; this makes it more brittle to change.
Real systems may involve hidden state: state that affects the system but is not directly set or seen. Consider this example:
This is an accumulator: it provides a running sum and average. Obviously, it must somehow keep track of n, the number of items entered, in order to compute the average.
We can make a declarative test out of this by exposing the hidden state to the test:
The first two columns, n and sum, represent existing state. Data is the number entered on the screen, and sum' and avg' are the results.
Getting to the hidden state can be a trick. Sometimes the programmers can explicitly expose it as part of a testing interface; other times the state can be accessed by a sequence of steps. In this case, we could setup the proper state for each row by doing this:
Hit the clear button
Repeat n-1 times: add 0
[Optional: check sum]
Then the rest of the row might be interpreted as:
This puts some burden on the test setup code. But if the setup is not done there, it's probably repeated in a bunch of scripts.
When to Use the Procedural Style?
Procedural style pays off best when the sequential nature of the feature is what's interesting: when how you got somewhere is as important as where you are. Consider the problem of multiple selection, using click, shift, control, and drag. For setup, imagine a vertical list of items, numbered 1 through 10. Consider this test:
Or this one:
You could perhaps develop a set of declarative tests for these, but the sequence of actions is what's interesting.
What to do?
Leverage procedural tests for the cases where the sequence of actions is paramount, and there's not a lot of commonality between scripts. Scenario tests can benefit from the procedural style.
A declarative test is like a table you could put in a user manual: it concisely and concretely explains a function. Declarative tests sometimes require extra setup, especially when hidden state is involved, but they're often worth that extra trouble. Declarative tests are good for testing permutations of a business rule.
If all else is equal, favor the declarative style.
These guidelines can boost your test-writing efficiency: they move repetitive actions into the test setup, and let you focus on the interesting part of a test.
[Written February, 2005. Brian Marick suggested the terms, though I'm not sure I picked the set he liked best.]