Unit Test and TDD Fundamentals

TDD results in unit tests. A unit test verifies the behavior of a code unit, where a unit is the smallest testable piece of an application. You typically write a unit test in the programming language of the unit.

Unit Test Organization and Execution

A single unit test consists of a descriptive name and a series of code statements, conceptually divided into four (ordered) parts.

  1. (Optional) statements that set up a context for execution

  2. One or more statements to invoke the behavior you want to verify

  3. One or more statements to verify the expected outcome

  4. (Optional) cleanup statements (for example, to release allocated memory)

Some folks refer to the first three parts as Given-When-Then. In other words, Given a context, When the test executes some code, Then some behavior is verified. Other developers might call this breakdown Arrange-Act-Assert (see Arrange-Act-Assert/Given-When-Then).

Typically you group related tests in a single source file. Google Mock, like most unit testing tools, allows you to logically group unit tests into fixtures. You might start with one fixture per C++ class being test-driven, but don’t let that constrain you. It’s not unusual to end up with more than one fixture for a single C++ class or one fixture that tests related behaviors from multiple classes. File Organization, contains further detail on files and fixtures.

You use one of dozens of available C++ unit testing tools to execute your unit tests. While most of these tools share many similarities, no cross-tool standards exist. As a result, C++ unit tests are not directly portable between tools. Each tool defines its own rules for how tests are structured, what assertions look like, and how tests get executed.

Using the tool to execute all our tests is a test run or suite run. During a test run, the tool iterates all tests, executing each in isolation. For each test, the tool executes its statements from top to bottom. When the tool executes an assertion statement, the test fails if the condition expected by the assertion does not hold true. The test passes if no assertions fail.

Unit testing practice varies widely, as does the granularity of the tests involved. Most developers writing unit tests (but not doing TDD) seek only the verification it provides. These developers typically write tests once they complete functional portions of production code. Such tests written after development can be tougher to write and maintain, primarily because they weren’t written with testing in mind. We’ll do better and use TDD.

Test-Driving Units

In contrast to plain ol’ unit testing (POUT), TDD is a more concisely defined process that incorporates the activity of unit testing. When doing TDD, you write the tests first, you strive to keep the granularity of tests small and consistent, and you seek myriad other benefits—most importantly the ability to safely change existing code.

You use TDD to test-drive new behavior into your system in quite small increments. In other words, to add a new piece of behavior to the system, you first write a test to define that behavior. The existence of a test that will not pass forces (drives) you to implement the corresponding behavior.

“Quite small?” No formal size constraint exists, so you want to move in the direction of an ideal instead. Each test should represent the smallest meaningful increment you can think of. Your favorite tests contain one, two, or three lines (see Arrange-Act-Assert/Given-When-Then) with one assertion (One Assert per Test). You’ll of course have plenty of tests that are longer, and that may be OK, but having an ideal will remind you to question the size of each longer test—is it doing too much?

Tests with no more than three lines and a single assertion take only minutes to write and usually only minutes to implement—how much code can you meaningfully verify in a single assert? These small bits of related new code you add to the system in response to a failing test are logical groupings, or units of code.

You don’t seek to write tests that cover a wide swath of functionality. The responsibility for such end-to-end tests lies elsewhere, perhaps in the realm of acceptance tests/customer tests or system tests. The term this book uses for tests against such aggregate code behavior is integration tests (see Unit Tests, Integration Tests, and Acceptance Tests). Integration tests verify code that must integrate with other code or with external entities (the file system, databases, web protocols, and other APIs). In contrast, unit tests allow you to verify units in isolation from other code.

The first rule of TDD (The Three Rules of TDD) states that you cannot write production code unless to make a failing test pass. Following the first rule of TDD, you’ll inevitably need to test-drive code that interacts with external entities. Doing so puts you in the realm of integration tests, but that may be OK; nothing about TDD prevents you from creating them. Don’t sweat the terminology distinction much. What’s more important is that you are systematically driving every piece of code into the system by first specifying its behavior.

In Chapter 5, Test Doubles, you’ll learn how to use test doubles to break dependencies on external entities. Your integration tests, slow by their nature, will morph into fast unit tests.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.218.69