20
Given-When-Then tests
Consider this test written in the "traditional" format in a test framework of the xUnit family:
/** @test */
public function calculatesOrderCost(): void
{
$itemA = Item::withPrice(20);
$itemB = Item::withPrice(60);
$shippingCost = ShippingCost::asPercentage(10);
$order = new Order();
$order->addItems($itemA, $itemB);
$order->applyShippingCost($shippingCost);
self::assertEquals(88, $order->totalCost());
}
In this post we will analyze the benefits of using the GWT format instead:
/** @test */
public function calculatesOrderCost(): void
{
$this->givenAnItemWithPrice(20);
$this->andAnItemWithPrice(60);
$this->andAShippingCostOfPercent(10);
$this->whenOrderIsCreated();
$this->thenTotalOrderCostIs(88);
}
Basically what happened is that we moved the different parts of the test to dedicated methods of any of the GWT types:
- The Given, meaning the state of the system required to execute the code under test.
- The When, meaning the execution of the code under test.
- The Then, meaning what is the expected output after executing the code.
Since each of these blocks is very likely to have more than one step, each of the blocks can include And methods as shown in the example.
GWT comes from BDD (Behavior Driven Development), an approach to testing coined by Dan North with a focus on behaviour, which in turn has lead to the creation of dedicated test frameworks (such as Cucumber, Behat or SpecFlow). In BDD, using GWT is the basis to write the test scenarios, whilst in xUnit frameworks is not that common to see this format and certainly more cumbersome to do. They're frameworks were not written with GWT in mind. However, using GWT there provides noticeable benefits as well.
What we achieve with the GWT format is:
1. Better readability
Making the tests resemble human readable descriptions of what our application does moves them towards the desirable state where our tests are "living documentation". They reflect what the system does and they will change along with the system when required, making them always up to date documentation.
2. Reduce the cognitive load
To get what's going on in the test we can read each step of it without the visual noise produced for the code required to implement it. The example is pretty naive for simplicity purposes, but we can easily imagine Item
or Order
would require a lot more parameters to be instantiated while we only care about the ones involving the cost in this case.
3. Verify the behavior of the system under test instead of its implementation
Testing the implementation instead of behavior is not desirable. It makes our tests and production code less prepared for refactoring and change. Because in GWT we describe what the system does, the test is less prone to fall in the trap of being coupled with the implementation.
It is important to note that GWT does not guarantee any of the above benefits, neither it means they cannot be achieved using another approach or other techniques. It just makes them easier to accomplish.
There are few things in software development that have no trade-off included, and BDD and GWT tests are not an exception.
BDD frameworks rely on Gherkin language to define the steps of a test in a separate feature file, and using regular expressions find the matching method implementation. For instance Given '1' items with price '80' euro*
will be matched with a method givenItemsWithPrice(int $amount, int $price)
. xUnit frameworks are not prepared for this type of pattern matching and the methods in GWT tests do not read as good as the pure BDD counterparts. In our xUnit tests we will have just the method implementation, not the more readable feature file.
Another aspect to consider is that when using GWT everything is encapsulated in separate methods and tests tend to store stuff in class properties to use them later on. This means tests may have more properties and state than what we may be used to, with later methods relying on properties set by prior ones. Sometimes this may produce null pointer exceptions and tests that are harder to follow:
/** @test **/
public function shipingToAddress(): void
{
$this->givenAnAddress();
$this->whenOrderIsShipped();
$this->thenPackageIsSent();
}
private function givenAnAddress(): void
{
// We need an address property
$this->address = new Address('sesame street');
}
private function whenOrderIsShipped(): void
{
$shippingService = new ShippingService();
// This method relies on an address previously set, producing a confusing error when it is not
$shippingService->shipTo($this->address);
}
Finally, GWT tests work better when we are testing code involving business rules. When testing classes more related to infrastructure, utilities, data manipulation,... GWT does not work that well and result in cumbersome tests that feel forced:
/** @test **/
public function sanitizeTextGWT(): void
{
$this->givenATextWithAccents();
$this->whenSanitizingTheText();
$this->thenAccentsAreRemoved();
}
/** @test **/
public function sanitizeText(): void
{
// Maybe GWT does not provide much value compared to this
self::assertEquals('aa', Sanitizer::sanitize('áà'));
}
As with every tool, the point is to use the it to solve our problems, not to bend the problems to force them into it.
Here are some techniques and soft rules that I found give good results when using the GWT approach in xUnit tests.
Do not use And
Using and at the beginning of a method reads pretty well when we write our first tests, but could lead to weird situations when a test needs the steps starting with and but none of the ones starting with Given, When or Then:
/** @test */
public function carReactsToAdverseWeather(): void
{
$this->givenThereIsMist(); // In this test rain is relevant
$this->andItIsRaining(); // So this reads nice
$this->whenWeatherIsScanned();
$this->thenMistLightsAreTurnOn();
$this->andDriveAssitanceOnWetRoadIsTurnOn();
}
/** @test */
public function carReactsToNotSoAdverseWeather(): void
{
$this->andItIsRaining(); // We don't care about the mist ant it reads weird
$this->whenWeatherIsScanned();
$this->andDriveAssitanceOnWetRoadIsTurnOn(); // Same here
}
Instead, avoid and prefix and use always given, when or then. It won't read as good as with and, but the benefit is greater than the loss.
/** @test */
public function carReactsToAdverseWeather(): void
{
$this->givenThereIsMist();
$this->givenItIsRaining();
$this->whenWeatherIsScanned();
$this->thenMistLightsAreTurnOn();
$this->thenDriveAssitanceOnWetRoadIsTurnOn();
}
Test error cases catching exceptions into a property
xUnit frameworks provide a fancy way of dealing with exceptions when throwing them is the expected behavior. However, it does not fit well in GWT because it would break the test structure or produce "technical sentences" in a otherwise human-readable test. A way of avoiding that is to use a try-catch inside the When method saving the exception in a property for a later, more meaningful assertion:
/** @test **/
public function shipingToAddress(): void
{
$this->givenAnAddress();
$this->givenClientHasNoFunds();
$this->whenOrderIsShipped();
$this->thenOrderFailedDuetoNoPayment();
}
private function whenOrderIsShipped(): void
{
try {
$this->shippingService->ship();
} catch (Exception $ex) {
$this->exceptionWhenShippingOrder = $ex;
}
}
private function thenOrderFailedDuetoNoPayment(): void
{
self::assertInstanceOf(NoPaymentException::class, $this->exceptionWhenShippingOrder)
}
Reduce the number of properties with return values and method parameters
The body of a GWT is composed of a bunch of methods, one after the other. If we do just that, we will be forced to use lots of properties to hold the values generated in by Given methods to use them in the following steps. This may hurt the readability of tests and makes them harder to follow because we cannot notice it unless we go into each of the methods, it is not visible explicitly in the test body.
/** @test */
public function calculatesOrderCost(): void
{
$this->givenAnItemWithPrice(20); // Requires storing a property for the item
$this->givenAnItemWithPrice(60); // Another property
$this->andAShippingCostOfPercent(10); // Another property
$this->whenOrderIsCreated(); // Will use the previously set properties
$this->thenTotalOrderCostIs(88);
}
But our methods can return stuff and accept parameters, and we can avoid overuse of properties playing with that (which is actually an advantage compared to BDD frameworks).
/** @test */
public function calculatesOrderCost(): void
{
$oneItem = $this->givenAnItemWithPrice(20);
$anotherItem = $this->givenAnItemWithPrice(60);
$shippingCost = $this->andAShippingCostOfPercent(10);
$this->whenOrderIsCreated($shippingCost, $oneItem, $anotherItem);
$this->thenTotalOrderCostIs(88);
}
Writing tests in GWT form could result in more valuable tests, because it makes easier to describe the behavior of the system and it kinda enforce us to write our tests the right way ™️. Give it a try to check how well it fits with you code style, always taking into account that, as like any other tool, it comes with a number of benefits but also trade-offs.
Hopefully this post has spark some curiosity. Let me know in the comments :)
PS: because I could not figure out something adequate as cover image, I've just randomly used a pic of one of the most mesmerizing animals out there, a Red Panda.
20