Published in


An example of example-driven development

This is an example of example-driven development using Glamorous Toolkit. The concrete code snippets are written in Pharo.

Examples vs. tests

Example-driven development is somewhat similar to test-driven development, but it differs both in how examples are expressed and composed, and in how examples are utilized both for testing and for documentation purposes.

Let us start from a concrete scenario of modeling a price to fulfill these requirements:

A price can be something like 100 EUR.
Prices can be added or multiplied.
A price can also be discounted either by a fix 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.

A possible design can look as follows. We have aPrice abstract concept that knows how to provide the price money, where money is an object that can hold something like 100 EUR . A ConcretePrice knows the concrete money, and this can be decorated in several ways, including PriceDiscountedByMoney, PriceDiscountedByPercentage, or even SummedPrice.

A possible object-oriented design for prices.

A simple SUnit test could look like this:

| price |
price := 100 EUR asPrice.
self assert: price = 100 EUR asPrice.

In this case, 100 EUR is a Money object that can understand asPrice to return a ConcretePrice. A more complicated test could look as follows:

| price |
price := 100 EUR asPrice discountedBy: 10 EUR.
self assert: price = 90 EUR asPrice

And yet more complicated:

| price |
price := (100 EUR asPrice discountedBy: 10 EUR) discountedBy: 10 percent.
self assert: price = 81 EUR asPrice

Ok, these are tests. In all three cases we setup an interesting object, we trigger a behavior, and we assert an expected outcome.

So, what’s an example then? An example is like a test, only it returns an object. You see, if we go through the trouble of creating an object, we might as well use that object for other purposes. So, our concrete price example would look like this:

| price |
price := 100 EUR asPrice.
self assert: price = 100 EUR asPrice.
^ price

The example method is annotated with <gtExample>, and it returns an interesting object, which in our case is price. And now we can build on top of this example:

| price discountedPrice |
price := self concretePrice.
discountedPrice := price discountedBy: 10 EUR.
self assert: discountedPrice = 90 EUR asPrice.
^ discountedPrice


| price discountedPrice |
price := self discountedByMoney.
discountedPrice := price discountedBy: 10 percent.
self assert: discountedPrice = 81 EUR asPrice.
^ discountedPrice

All in all, what we can express in tests we can also express through examples. Similar to tests, examples can be independently assertable. But, unlike tests, examples can be explicitly composed of arbitrary other examples, and the composition can be arbitrarily nested. One implication of this explicit composition is that we can order examples, and when multiple examples fail, we can point out to the most specific failure. For example, if in our case, the concretePrice example would fail, the other two examples would fail as well. However, when presenting the results, we can easily point to the root problem exposed in concretePrice.

Similar to tests, examples can be independently assertable. But, unlike tests, examples can be composed of arbitrary other examples.

Beyond tests

In a typical test-driven development scenario, a test is interesting as long as it fails. That can happen both at the beginning when we write first the test and there is no code, and when a regression occurs. As soon as the test succeeds, it is no longer utilized outside of checking for regressions.

An example that passes its assertions represents a new opportunity for prototyping, for constructing new examples, for supporting software assessment or for documentation.

Enhancing examples through the development environment

Let’s go back to our scenario, and let’s suppose that we do not know how the price logic is internally implemented. We can look at the code, but at the same time we can also look at examples of concrete objects.

First, if we look at the code of an example, the environment allows us to unfold the code of the other examples used so that we can understand how the larger object is statically created. In our case, we have the concretePrice that was first discountedByMoney and was finally discountedBy: 10 percent.

The environment takes advantage of fine-grained example nesting and helps us understand how the larger example is statically composed out of the smaller ones.

Running the example, gives us the resulting PriceDiscountedByPercentage instance that we can inspect in details. We see that it relates to an original price of 90.00 EUR which, when inspected reveals that it is a PriceDiscountedByMoney instance with an original price of 100.00 EUR.

Executing the example allows us to inspect the resulting object.

But, we can go a step further. Glamorous Toolkit is a moldable environment which means that any fact about our system can be represented in multiple ways. In our case, it would be great to see the nesting of the price operations and see the intermediate results as well.

The Inspector offering a custom view for understanding how price operations are nested.

The view from above costed a few dozen lines of code. It’s like printing a string, only it‘s much richer. The power of this view comes precisely from being hand-crafted to reveal the specific design of our objects.

When combined with this view, the value of our example has soared because it is now directly useful as an assessment tool. For example, take a look at the example below showing a couple of other operations.

Inspecting a complicated price involving multiple operations.

Could you get an idea of the inner workings of how the inspected price object is structured internally? How does it feel?

Examples as exploration starting points

Exploration and prototyping are forms of feedback. Existing examples provide basic pieces that can be assembled quickly into new ones. For example, below we see a Playground in which we take two existing example objects and create a new one that we can explore.

Combining example objects into a new one in Playground and exploring the result in the Inspector.

Examples as units of documentation

Once we get inspector views that can tell interesting stories about our example objects, it’s only natural to want to combine those views into broader narratives. For example, below we can see a live document edited in Documenter in which we add live our discount example, and can trigger the preview right in place.

Adding an example to a live document created in Documenter.

There are at least two things we can take away from this. First, the cost of assembling interesting documentation is marginal once the examples exist. Second, as there is no copy pasting of code, the documentation is both testable and maintainable.

Why call them examples

Let’s look at this again:

| price |
price := 100 EUR asPrice.
self assert: price = 100 EUR.
^ price

On the one hand, executing the code gives us a valid object which acts as an example of all possible valid objects. One the other hand, the code itself exemplifies both how to construct the object, and what concrete contract that object adheres to. In other words, both the result and the code that produces the result can be seen as examples. Hence the name.

Examples offer a simple and direct model of composition that lends itself well both for supporting testing and for enabling prototyping. And when combined with a moldable development environment, examples also serve as units of documentation.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tudor Girba

Tudor Girba

CEO | Software Environmentalist @feenkcom. Making software systems explainable.