Example-Driven Development

10 minute read

Published:

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.

fixedGame example method

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.

A Memory game live view

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.

chooseMatchingPair example method

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.

Memory game after one matched pair

We can also reuse it as a setup for yet another example, in this case, playToEnd.

playToEnd example method

If we switch to the Examples map view, we can see all the dependencies between the examples.

An Example map for the memory game

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.

The Money class hierarchy

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:

A Money instance

While:

42 euros + 10 usd.

yields:

A MoneyBag instance

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.

Adding zero money example

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.

A non-existing class with a fixit

The ConcretePrice class does not exist, so we see a fixit (wrench) icon. We click on it to generate the list of fixit options.

A fixit dialog

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.

Creating a new class

We also generate accessors as a standard code transformation.

Creating accessors

Now we have a ConcretePrice instance to explore!

Inspecting a ConcretePrice instance

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.

Prototyping the asPrice behavior

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.

Our code extracted as an extension method

And now we can simply write 100 euros asPrice.

A factory method for prices

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.

Defining a new view

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.

The Money view installed

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.

The Extract example refactoring

We introduce a new class to hold our examples, and give the example a suitable name.

An extracted example method

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.

A failing test case

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.

Comparing 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.

Comparing Price equality, not identity

Now we can go back to the example and rewrite it to add the new assertion.

Our assertion as part of the example

Price Examples

After a number of iterations we end up with something like this, with a hierarchy of examples covering test cases for prices.

The Price Examples map

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.

Embedding a live Money Bag as an example

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.

Embedding a live discounted Price as an example

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!


This article was originally posted on the feenk blog.