Specification By Example
Written by Ivan Dugalic - July 2022
According to Gojko Adzic, the author of ‘Specification by Example’, Specification by Example is a set of process patterns that facilitate change in software products to ensure that the right product is delivered efficiently.”
- It is a collaborative approach to software analysis and testing.
- It is the fastest way to align people from different roles on what exactly we need to build and how to test it.
Illustrating requirements using examples
The requirements are presented as scenarios.
A scenario is an example of the system’s behavior from the users’ perspective,
and they are specified using the Given-When-Then
structure to create a testable specification:
- Given
< some precondition(s) >
- When
< an action/trigger occurs >
- Then
< some post condition >
We face business with specific questions they should be able to answer. We are not facing them with abstractions or generalizations. We are dealing only with data that are formally representing preconditions (events), actions (commands) and post conditions (new events):
- Given
< some event(s) / current state of our system
- When
< an command occurs >
- Then
< some new event(s) / evolves to the new state of our system >
It also represents an acceptance criterion of the system and acts as a documentation.
Refining specifications
It is important to realize that we need to go through all the scenarios, successes and errors.
For example, with given OrderPlaced
and OrderAccepted
events as a precondition, when command MarkOrderAsPrepared
is triggered, then Order is successfully prepared (OrderPrepared
).
But, with only OrderPlaced
given as precondition (without OrderAccepted
), handling the same command MarkOrderAsPrepared
will produce different result/failure (OrderNotPrepared
).
It means that order can be marked as prepared only if it was previously accepted.
Automating tests based on examples
We are decoupling the decision-making logic (domain layer) from the state management logic (application/infra layer). This allows keeping the cognitive load low.
Our main focus is the decision-making process formalized as a set of pure functions. Functions/lambda offers the algebra of manipulating the data (commands, events, state) in a compositional manner, effectively modeling the behavior. This leads to modularity in design and a clear separation of the entity’s structure and functions/behaviour of the entity.
‘FModel library’1 offers generic and abstract components to specialize in for your specific case: Decider
, View
, Saga
. You can read more about it here.
You can create a small DSL in Kotlin to write and run specifications in Given-When-Then
structure (testable specification):
private fun <C, S, E> IDecider<C, S, E>.givenEvents(events: Iterable<E>, command: () -> C): Flow<E> {
val currentState = events.fold(initialState) { s, e -> evolve(s, e) }
return decide(command(), currentState)
}
private fun <C, S, E> IDecider<C, S, E>.whenCommand(command: C): C = command
private suspend infix fun <E> Flow<E>.thenEvents(expected: Iterable<E>) = toList() shouldContainExactly (expected)
Kotest framework is used in these tests, but you are not required to use Kotest. You can implement these three functions in your favourite test framework/library.
Runnable tests:
test(“Place Order") {
with(orderDecider) {
givenEvents(emptyList()) {
whenCommand(PlaceOrderCommand(...))
} thenEvents listOf(OrderPlacedEvent(...))
}
}
test(“Accept Order - Success") {
with(orderDecider) {
givenEvents(listOf(OrderPlacedEvent(...))) {
whenCommand(AcceptOrderCommand(...))
} thenEvents listOf(OrderAcceptedEvent(...))
}
}
test(“Accept Order - Error") {
with(orderDecider) {
givenEvents(emptyList()) {
whenCommand(AcceptOrderCommand(...))
} thenEvents listOf(OrderRejectedEvent(...))
}
}
Notice how the decision-making process (a Decider
) is driven by data in which you make new decisions (events) based on the current state (current events) and action (command) being triggered:
decideFunction: (givenEvents, whenCommand) -> thenEvents
This function is the heart of the Decider
component.
You can find more information and test examples within FModel library itself.
In the next blog post we are going to explain how we can use ‘event modeling’2 as a method to initiate collaborative process providing us with a blueprint of the information system, and how we can refine our scenarios to make it effective - event modeling is specification by example.
-
FModel can be used as a library, or as an inspiration, or both. It provides just enough tactical Domain-Driven Design patterns, optimised for Event Sourcing and CQRS. ↩
-
event modeling is a method of describing systems using an example of how information has changed within them over time. ↩