Chapter 10. BDD and unit testing

 

This chapter covers

  • The relationship between BDD, TDD, and unit testing
  • Going from automated acceptance criteria to implemented features
  • Using BDD to discover design and explore low-level requirements
  • Tools that help you write BDD unit tests more effectively

So far we’ve focused on Behavior-Driven Development (BDD) as a tool for discovering, illustrating, and verifying business requirements. But BDD doesn’t stop at the business requirements level, or once you’ve automated your acceptance tests. In this chapter you’ll learn how BDD principles and tools can help you write better-designed and better-tested application code (see figure 10.1).

Figure 10.1. In this chapter we’ll focus on using BDD practices at the unit-testing level.

The principles of BDD can be effectively applied at all levels of development, and with significant benefits:

  • BDD is about writing executable specifications that guide the implementation at all levels of development.
  • At a unit-testing level, BDD builds on and extends established TDD practices.
  • BDD practitioners use an outside-in approach, using automated acceptance tests and unit tests to drive the implementation of the underlying code.
  • You can practice BDD-style unit testing with any tool, but some tools make it easier to write more expressive and more concise unit tests.
  • BDD practices at the unit-testing level also help provide living technical documentation of the components and APIs you develop for your application.

10.1. BDD, TDD, and unit testing

The cornerstone of TDD is the idea of writing a unit test before you write the corresponding code. But TDD is much more than just a guarantee that every class has a corresponding set of unit tests. With enough discipline, any experienced developer writing unit tests after writing the code can achieve that outcome as well. TDD’s killer feature is that it forces developers to think about the code they’re going to write before they write it, in practical and unambiguous terms—you need to understand the functionality before you can write a unit test for it. This way, developers resist the temptation to just start coding something and actively think about what they need to achieve. In this respect, a better term for this practice might be something like “Test-Driven Design.”

Experienced developers will typically think about the design of their code before they do any coding, but expressing this design in the form of unit tests makes this process a lot more concrete. Before implementing any code, TDD practitioners imagine the code “they would like to have,” which tends to result in cleaner, better-designed APIs. These unit tests also become examples of how to use the application code. And because at least one test is written for every new feature, the code tends to be better tested and have significantly fewer bugs.

10.2. Going from acceptance criteria to implemented features

In this section we’ll walk through a typical BDD workflow, going from high-level automated acceptance criteria to application code. We’ll keep the requirements simple, so that we can focus on the process. But before we start, let’s take a look at what we mean by “outside-in” development.

10.3. Exploring low-level requirements, discovering design, and impleme- enting more complex functionality

The acceptance criterion you implemented in the previous section was a very simple one, and it didn’t require much in the way of unit tests or underlying code complexity. But this is usually not the case. In real-world applications, many acceptance criteria hide a large amount of complexity under the hood. In some cases, the automated acceptance test may be sufficient to illustrate and verify this functionality; in other cases, each step in the high-level acceptance criteria may lead to a large number of low-level requirements that you’ll express as BDD unit tests (see figure 10.5). This is typical of the BDD work process: as you implement the code required for an acceptance criterion, you’ll often discover more low-level requirements that you’ll also need to implement.

In this section we’ll look at some of the techniques used when you expand high-level acceptance criteria into more detailed unit tests, discovering the classes, methods, and services you need as you go.

10.4. Tools that make BDD unit testing easier

Other unit-testing tools, such as RSpec, NSpec, and Jasmine, put the emphasis on writing low-level executable specifications in the form of unit tests, which encourages a more rigorous, test-first approach.

More recent BDD unit-testing tools such as Spock and Spec2 allow for more expressive and powerful low-level specifications, including example-driven specifications.

10.5. Using executable specifications as living documentation

From a BDD perspective, writing a good unit test is an exercise in good communication. When you practice BDD, you think of every unit test as a low-level specification that illustrates some aspect of how a class or component behaves. You’ve seen ways to help ensure that the intent of these low-level specifications is clearer. But the implementation of your test is also sample code that illustrates how a particular requirement is satisfied, or how a particular goal is achieved. The code inside your tests doesn’t just exercise the application; it documents how to exercise the application.

Not keeping your test code clean and easy to understand has very practical consequences. If an old test fails when you make a change elsewhere in the code base, it’s essential to understand both what the test was trying to demonstrate and how it was doing so. Only by understanding this will you be in a position to decide what to do with the failing test.

10.6. Summary

In this chapter you learned about practicing TDD techniques using a BDD style:

In the next chapter, we’ll look at how BDD fits into the broader picture of project management and reporting, and how to get the most out of your living documentation.

sitemap