EDDSlideshow

Example-Driven Development

Example-Driven Development is superficially like Test-Driven Development, where you drive development by constructing tests 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.

An example of an example ...

Here we see a simple example of an example method. 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 not only to carry out the tests, but also to inspect the result.

Explain the method. Inspect the result. Play the game. Explore the views.

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.

Browse the chooseMatchingPair example. Open the fixedGame code bubble. Inspect the result. Browse senders of chooseMatchingPair. Browse playToEnd. Explore the Examples map .

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

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.

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.

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.

All these classes have a common abstract Money superclass for shared behavior.

Inspect the 42 euros snippet. Inspect the 42 euros + 10 usd snippet. Click on the GtTZeroMoney class. Click on the GtTMoney class.

Money examples

The money classes are heavily covered by examples, which are essentially unit tests that also return example objects.

This means that 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.

Run all the examples. Inspect the first example. Open the code bubbles to see how they are composed. Go to the Examples map to show all the dependencies.

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.

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. Now we have a first concrete Price object!

Create the ConcretePrice class as a fixit. Give it the EDDPrices package, the Model tag, and a money slot. Create the accessors. Change the argument of money: to aMoney. Inspect the result. Inspect its money slot.

Create a factory method

We would like to be able to create a price object by sending asPrice to a Money instance.

We start by inspecting the Money instance. We prototype the code to create the Price instance. We try it out. And we extract the factory method. We change the method to be an extension from the EDDPrices package.

Now we can simply write 100 euros asPrice.

Inspect the money. Open the playground. Code up ConcretePrice new money: self; yourself Inspect the result. Extract the asPrice method. Browse the new method. Change the category to *EDDPrices. Go back to the page and change the code to 100 euros asPrice and inspect it.

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

Inspect the price object. Inspect the money slot and view the Details view code. Copy the method name. Go back to the Price inspector and switch to the Meta view. Add a forwarding gtMoneyFor: method. Show the new view.

Extracting an example

At this point it looks like we have a nice example for testing, so let's extract it as an example.

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.

Select all the code, right-click and Extract example Set the receiver to PriceExamples and the selector to hundredEuros. Choose EDDPrices as the package and Examples as the tag. Accept the refactoring. Inspect the result. Browse the code bubble.

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.

Let's have a look at the = method. It's testing for object identity. Now 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. Now we can go back to the example and add a test.

Inspect the example and open the playground. Evaluate other := 100 euros asPrice. self = other and see it fails. Open the code bubble for = (alternatively search for = in the Meta view). Evaluate a new snippet self money = other money and see it passes. Browse the = method of money and see it has been overridden. Rewrite the hundredEuros example, adding the assertion. Use an Extract temporary refactoring to assign the example expression to a variable price, and then express the assertion over that.

Price Examples

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

Run all the examples. Show the longest chain, i.e., for displayOfConcretePriceDiscountedByMoneyAndDiscountedByPercentage and inspect the result. Show the Overview with the price composition.

EDD in a Nutshell

Summing up, instead of starting by writing a test, we first create a live object to explore.

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. We iterate until we're done!