Example-Driven Development
What is EDD?
Example-Driven Development is superficially like Test-Driven Development, where you drive development by constructing test methods that return example objects. It sounds simple, but it actually changes the development process in several fundamental ways.
The Trouble with TDD
With TDD, you develop code by incrementally adding a test for a new feature, which fails. Then you write the “simplest code” that passes the new test. You add new tests, refactoring as needed, until you have fully covered everything that the new feature should fulfil, as specified by the tests.
But: Where do tests come from? When you write a test, you actually have to “guess first” to imagine what objects to create, exercise and test.
How do we write the simplest code that passes? A test that fails gives you a debugger context, but then you have to go somewhere else to add some new classes and methods.
What use is a green test? Green tests can be used to detect regressions, but otherwise they don't help you much to create new tests or explore the running system.
With Example-Driven Development we try to answer these questions.
What's an Example?
An example method is just a test method that happens to return the object being tested. Through this simple change, instead of a passing test simply being green, we get back an object that we can inspect, explore, and reuse for various purposes.
Here we see a simple example of an example method for a “Memory” game. It is annotated with a <gtExample>
pragma to flag it as an example.
Like any test, it has a setup, which in this case creates a game
object. We check some assertions, in this case perform no further operations, and then we return the object under test.
This allows us not only to carry out the tests, but also to inspect the result. Here we see a screenshot of the Live GUI view of the memory game instance.
Composing examples
Once we have an example, we can also use it as a setup for another example.
chooseMatchingPair
is another example method that starts with fixedGame
as its setup.
As in a conventional test, we can check some preconditions, perform one or more operations, and then check some postconditions. The difference, again, is that we return the object under test, so we can explore it.
We can also reuse it as a setup for yet another example, in this case, playToEnd
.
If we switch to the Examples map view, we can see all the dependencies between the examples.
What are example methods good for?
As we have seen, examples make dependencies between tests explicit by reusing examples as setups for other examples, thus forming a hierarchy of examples.
• Example composition reduces:
— code duplication,
— cascading failures.
• Examples can be reused in live documentation.
• EDD is an exploratory approach to TDD.
Best practice in test design supposedly should avoid dependencies between tests, but studies have shown that this practice instead leads to implicit dependencies due to duplicated code in test setups. This in turn leads to cascading failures due to the same setups being repeated in numerous tests. By factoring out the commonalities as examples, the duplication is removed, and cascading failures are avoided.
A further benefit is that examples can be used in live documentation, and, as we shall see, examples support an exploratory approach to test-driven development, that we call example-driven development, or EDD.
Modeling prices
Let's work through an example where we want to model prices for goods, that may be discounted by fixed amounts, or percentages, or even combinations of different types of discounts.
A price can be something like 100 EUR. Prices can be added or multiplied. A price can also be discounted either by a fixed amount of money, or by a percentage. All operations can be combined arbitrarily. And for audit purposes, we want to track all operations that lead to a concrete amount of money.
Money classes
To simplify our task, we assume that we already have classes that model different amounts of money, such as 42 € or 10 USD.
All these classes have a common abstract GtTMoney superclass for shared behavior.
An amount of money is always in a currency such as euros or US dollars. A bag of money consists of amounts of mixed currencies. A zero amount of money doesn't have a currency.
This expression:
42 euros.
yields:
While:
42 euros + 10 usd.
yields:
Money examples
The money classes are heavily covered by examples, which are essentially unit tests that also return example objects. For example, this method tests that adding a zero amount of a different currency won't accidentally create a bag of monies.
A passing test is not just green, but also returns an object that can be explored, reused as a setup for another example, or embedded into live documentation. Unlike tests, however, examples don't come “first” but they are extracted during the example-driven development process.
Introducing a Concrete Price
Just like we have a hierarchy of Money classes, we expect to end up with a hierarchy of Price objects, including an abstract root class, a concrete, fixed price, and several kinds of discounted prices. Instead of designing this hierarchy up-front, we'll develop it incrementally, driven by examples.
We'll start with an example of a concrete (as opposed to an abstract) Price object.
A price can be something like 100 EUR. Prices can be added or multiplied. ...
Start from an object
Instead of starting by imagining and writing a test case as an example method, we start by creating an instance of the class we need. We first simply ask how we want to create our concrete instance of a price, and we write that code in a snippet.
Neither the class nor the constructor exist, so we create them as fixit operations. We start with a snippet to create an instance of the class we want to design.
The ConcretePrice
class does not exist, so we see a
fixit
(wrench) icon. We click on it to generate the list of fixit options.
We fill in the form to create a new ConcretePrice
class in the EDDPrices
package, tag it as a Model
class, and assign a money
slot. We click the Create
button to perform the fixit.
We also generate accessors as a standard code transformation.
Now we have a ConcretePrice
instance to explore!
Create a factory method
We would like to be able to create a price object by directly sending asPrice
to a Money instance. We prototype this behavior in the playground of the GtTCurrencyMoney
instance we have in front of us.
Now we can perform an
Extract method
refactoring on this code snippet, and change its category to be an
extension method
from our EDDPrices
package.
And now we can simply write 100 euros asPrice
.
Adding a view
Our new Price object has only an ugly generic view, but its money slot has a nice view we could reuse.
We go to the Meta
view of the Price object and add a new view method that forwards itself to the Details view of its money
slot.
A view is just a method that takes a view object as an argument, has a <gtView>
pragma, and uses the view API to create the view we want, in this case a forward
view. We set the title of the view to Money
, and the priority to 10 so it appears early in the list of views. The object we want to forward to is the money slot, and the view is its gtDisplayFor:
view.
The moment we commit the view code, the view becomes available.
Extracting an example
At this point it looks like we have a nice example for testing, so let's extract it as an example by applying an Extract example refactoring.
We introduce a new class to hold our examples, and give the example a suitable name.
Note that the extracted example method has a <gtExample>
pragma, and unlike a usual test method, it returns an instance.
Adding assertions
We now have an example, but we aren't testing anything yet.
Rather than directly adding tests to the example method, let's explore first. We expect that a price object should equal another price object with the same money value. We see that this fails.
If we look at the =
method and see that it's testing for object identity, not object equality. Let's see what happens if we directly compare the money
slots.
This passes. We see that Money has implemented =
, so we should do the same. We have the code we want right here, so let's extract it as a new =
method.
Caveat:
actually there is a bit more work to do to implement a proper =
method, but let's skim over this point.
Now we can go back to the example and rewrite it to add the new assertion.
Price Examples
After a number of iterations we end up with something like this, with a hierarchy of examples covering test cases for prices.
Embedding examples in live documentation
An important benefit of examples is that they can be embedded within live notebooks to document significant use cases and scenarios. Here we see not just the source code, but the live example of a Money bag being used to document the price model.
And within the same notebook page, we see a live example of a multiple-discounted Price object, with a view that documents each discounting step.
EDD in a Nutshell
Summing up, instead of starting by writing a test, we first create a live object to explore.
• Start with an object
— Prototype behavior in the playground
— Extract methods
— Introduce useful views
• Extract examples
— Prototype assertions in the playground
— Add them to the example method
— Reuse examples as setups for new examples
— Embed examples within live documentation.
We prototype any behavior in the playground of the live object, and then extract methods that work. We create views that explain what is interesting about the object.
We extract interesting instances as example methods of a dedicated examples class. We prototype tests in the playground of the live example, before adding them as assertions to an example. We reuse the examples as setups for new examples, and as live documentation.
We iterate until we're done!