Unit Tests as First-class Entity in Programming Languages
Monday, November 23rd, 2009
— To free unit-tests from the slavery of objects, horray!
Languages should support unit tests as first-class entities. In a recent blog post Cedric Beust, creator of TestNG, advocated the introduction of a unittest keyword that would allow test methods to be put in the class under test rather in a separate file. He pointed to the D language doing this.
In fact, the Noop language takes first-class unit testing even further
We believe that test code looks different from production code. Tests should be very simple, with minimal conditional logic, so that they may be read as documentation. […] A test is an entity, distinct from a class or method whose purpose is to run tests. […] suite is just another test block enclosing some tests, and may be arbitrarily nested, and divided among several files.
RSpec and friends are of course a further example of first-class unit testing. Again, tests are distinct from classes or methods and are enclosed in possibly nested contexts. As in
describe Bowling do
it "should score 0 for gutter game" do
bowling = Bowling.new
20.times { bowling.hit(0) }
bowling.score.should == 0
end
end
In the following, I present my vision of 1st-class unit-tests.
Organizing tests in methods and classes is silly. In fact, organizing unit-test in methods and classes is an artifact of Smalltalk’s developed environment. (Yes kids, SUnit predates JUnit.) In Smalltalk, the only code that can be edited are methods bodies. So let’s see what we need to run tests
- an example
- that runs in
- a context
An example specifies part of the unit under test. An example should be executable code, such that we can automatically check if the unit under test implements the specification. When example code runs, it runs within a given context (more on that later). The example code can access all state of the context, which thus provides the test fixture.
Let’s nail down the distinction between “example” and “test” first.
In the same way as class and method are static templates for the runtime entities object and (method) activation. Test code is the static template for the runtime entity example. In the same way that not only classes and methods but also object and activation are the same, an example is the same as an activation of an object. It is a chunk of memory with enough space for each fixture variable and a pointer to its origin.
We can summarize this in a table to make the analogy to classes and methods more apparent.
| Template | Realization | Entity | Context |
| Class | creating | Object | Instance of outer class |
| Method | invoking | Activation | Activation of calling context |
| Test | running | Example | Cloned example of producer |
Upon execution of a test, a new example is allocated, linked to the example of its context, and then the test code executed. So the context of a test is just the example that resulted from another test execution. With “resulted from” we mean the state of the example’s memory chunk after running that other test has terminated. We refer to that other test as “producer” since it provides the fixture for the to-be-run “consumer” test.
So how are producer and consumer linked to each other?
Here we differ from classes and method. Object are (if your language-of-choice supports inner classes) linked by the lexical context of their classes. Activations are linked by the dynamic context of the executed methods. For testing however, we need way more flexible ways of setting up the testing harness. We should be able to create examples based on any possible combination of tests.
Of course, nesting of providers as given in RSpec and Noop will be one very common case. But certainly as common will be the case where a chain of test that executed multiple time with a different outer-most provider. In JUnit this is typically achieved by subclassing a test class and overriding either setup or another helper method. An example of this case is when you have an abstract test class for Collection that tests the collection interface and a test subclass for each concrete collection subclasses that just creates another instance of that subclass.
However we might think of a more complex setup, as for example a test harness that uses the same graph of tests to test the combination of n server examples cross m client examples.
So what we need as flexible way to connect tests with the examples of other tests.
Think of JExmple 2.0 as a test harness DSL.
This DSL will be provided by tool builders, what the language has to provide is first-class entities for tests, examples and test running (which takes as parameters a test and an example). Of course, the language should also allow to clone both examples and objects, such that tool builder can avoid side-effects when multiple test expand on the same example.
