DEV Community

Cover image for Given-When-Then tests
Carlos Gándara
Carlos Gándara

Posted on

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());
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

What is GWT

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.

The benefits

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.

Trade-offs and considerations

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);
}
Enter fullscreen mode Exit fullscreen mode

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('áà'));
}
Enter fullscreen mode Exit fullscreen mode

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.

Tips and tricks

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
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

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.

Discussion (0)