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 simple SUnit
test could look like this:
PriceTests>>testConcretePrice
| 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:
PriceTests>>testDiscountedByMoney
| price |
price := 100 EUR asPrice discountedBy: 10 EUR.
self assert: price = 90 EUR asPrice
And yet more complicated:
PriceTests>>testDiscountedByMoneyAndDiscountedByPercentage
| 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:
PriceExamples>>concretePrice
<gtExample>
| 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:
PriceExamples>>discountedByMoney
<gtExample>
| price discountedPrice |
price := self concretePrice.
discountedPrice := price discountedBy: 10 EUR.
self assert: discountedPrice = 90 EUR asPrice.
^ discountedPrice
and:
PriceExamples>>discountedByMoneyAndDiscountedByPercentage
<gtExample>
| 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
.
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
.
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 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.
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.
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.
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:
PriceExamples>>concretePrice
<gtExample>
| 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.