TDD, BDD, and specs2: A testing strategy for Scala

Here at YOOX NET-A-PORTER, we think that testing is an important part of a modern development process. As a new starter in a team working on Scala APIs that provide up-to-date shipping options information, I wanted to write about ideas we’ve discussed on how we write test code. Writing good test code can be as much of a skill as writing good application code, and our team have been looking at tools and frameworks to help us with writing the most readable, reliable and efficient test code that we can.

TDD

First things first, a word on TDD. TDD stands for Test-Driven Development, the idea of writing test code before writing application code. During TDD, a developer will start by writing a test that will definitely fail (normally because the application code hasn’t been written yet). Next, the developer writes just enough code to make the test pass. Finally, the developers goes back and rewrites the application code in stages to add in necessary functionality. It’s a cyclical method of writing code based on writing tests first, rather than adding them into your application code afterwards. The term has been in use since around 2003, thanks to Kent Beck’s book “Test-Driven Development by Example“.

BDD

Next, an explanation of BDD. BDD stands for Behaviour-Driven Development, and builds on the idea of TDD by deciding which tests to write first. BDD in an approach to testing where the system is described in terms of expected behaviour of the system, using the format:

GIVEN... (describe the pre-conditions and setup)
WHEN... (describe the action taken by the user)
THEN... (described the expected outcome)

By approaching tests in this way, BDD helps a developer work out what tests to write, and to frame the tests in terms of the functionality. An example BDD test using Given-When-Then syntax could be as follows:

Scenario 1: Calling the product details endpoint should return information on a single product 
Given that a user has a valid item identifier
When the user calls the product details endpoint with that identifier
Then a full set of product information should be returned

BDD encourages developers to think of user outputs and inputs to the application, rather than getting caught up too soon with implementation details. This is important because in theory we want to be able to swap implementations without meaning that our tests no longer accurately document our code. So for example, any time we write code that says:

GET /item/12345
gets item 12345

Then if we decide that we want to change the endpoint to “/sku/12345”, our tests are no longer documenting our code – although an IDE can help with renaming methods even within strings, they can struggle with widely used words such as “get”.

A clearer description might be:

Given that a user has a valid item identifier
When the user calls the product details endpoint with that identifier
Then a full set of product information should be returned

This new test description gives more detail of expected behaviour without coupling the test to the application code via method names. Additionally, when running the tests, a readable documentation of expected application behaviour is generated.

specs2

Moving on from methodology to actual code tools, specs2 is a widely used testing framework for Scala, that comes bundled as part of the Play Framework.

The specs2 library is a solid, well-supported and well-used framework for writing tests in Scala. specs2 support two distinct ways to write test codes, allowing for a range of different test formats within a single project. From the project’s own homepage, we can see that both of these styles would be valid test classes using Scala and specs2:

specs2_1

specs2_2

Although both of these styles are valid and readable, it can be difficult in a large team of developers to maintain a consistent style of testing that can be read and understand easily by all the rest of the team.

specs2 with BDD

One solution can be to use a BDD-style syntax combined with the helper functions that specs2 provides to support this. The advantages of this are:

  1. Tests within the codebase provide a simple and self-documenting style
  2. Test output provides a clear documentation of the expected application behaviour

Let’s write some examples. First we define a class of a product details endpoint:

class ProductDetails {
  def get(id: String): String = {
    // Returns product info
  }
}

Then we can write a simple test for this as follows:

class ProductDetailsSpec extends Specification {
  override def is =
    s2"""
      get
        gets $e1
    """

  protected val validId = "12345"
  protected val productEndpoint = new ProductDetails()

  def e1 = {
    productEndpoint.get(validId) must beEqualTo("Some valid product information")
  }
}

Whilst this test is complete and tests our code accurately, it doesn’t provide a very full explanation of the expected behaviour, and it doesn’t define the preconditions of success.
We can refine it further, using the specs2 operator “>>”, which is described in the documentation as follows:
A unit specification extends org.specs2.mutable.Specification and uses the >> operator to create “blocks” containing Texts and Examples:

We can then create a test that reads as follows:

class ProductDetailsSpec extends Specification {
    protected val validId = "12345"
    protected val productEndpoint = new ProductDetails()

  "get should" >> {
    "get product info" >> {
      productEndpoint.get(validId) must beEqualTo("Some valid product information")
    }
  }
}

This is better, as it provides a more helpful test output, and documents the expected behaviour of the system.

We can then improve the readability of the test even further by leveraging some of the features of specs2 to more accurately reflect BDD-style syntax.

Firstly, we can replace the operator “>>” with the word “should”, which improves the readability. Looking at the definition of the “should” operator within specs2 shows that it acts as a wrapper around the “>>” operator, also adding the text string of “should” within the test output.

Then, we can extend the specs2 Scope trait, which provides a context within which the test should run, and which is created fresh for each individual test within as suite. This allows us to neatly define the context of a given test, whilst ensuring that set-up and tear-down are done per-test.

By the making these two changes, we can modify the description of the test as follows.

class ProductDetailsSpec extends Specification {
  "Given that a user has a valid item identifier, call the product details  
   endpoint" should {
    "return a full set of product information" in new Given {
      productEndpoint.get(validId) must beEqualTo("Some valid product information")
    }
  }

  trait Given extends Scope {
    protected val validId = "12345"
    protected val productEndpoint = new ProductDetails()
  }
}

This gives us a clearly formatted test, which follows good practice in terms of encapsulation, and which provides a clearly human-readable format that can be used by both developers, and non-developer roles such as QA and product, to produce clear and descriptive test output.

Summary

Behaviour-Driven Development isn’t for everyone, and may not feel appropriate for every situation. Our team hasn’t been fully convinced that BDD syntax is useful or helpful for all tests within an application, and we’ve had some interesting discussions on this subject. However, when it does feel appropriate – such as working on a new features with clearly defined business logic – the specs2 test framework with Scala gives a great set of tools to write BDD tests. specs2 with BDD syntax not only provides the ability to comprehensively test your code, but also produce a useful, human-readable documentation of the expected behaviour of an application.

Print Friendly

Leave a Reply