Mind-Sets for Successful Adoption of TDD

TDD is a discipline to help you grow a quality design, not just a haphazard approach to verifying pieces of a system. Success with TDD derives from adopting a proper mentality about how to approach it. Here are a half-dozen useful mind-sets to adopt when doing TDD.

Incrementalism

TDD is a way to grow a codebase from nothing into a fully functional system, bit by bit (or unit by unit). Every time you add a new unit of behavior into the system, you know it all still works—you wrote a test for the new behavior, and you have tests for every other unit already built. You don’t move on unless everything still works with the new feature in place. Also, you know exactly what the system was designed to do, because your unit tests describe the behaviors you drove into the system.

The incremental approach of TDD meshes well with an Agile process (though you can certainly use TDD in any process, Agile or not). Agile defines short iterations (typically a week or two) in which you define, build, and deliver a small amount of functionality. Each iteration represents the highest-priority features the business wants. The business may completely change priorities with each subsequent iteration. They may even choose to cancel the project prematurely. That’s OK, because by definition you will have delivered the features that the business wanted most. Contrast the result with a classic approach, where you first spend months on analysis and then on design, before proceeding to code. No real value is delivered until well after you complete analysis and design and usually not until all coding is considered complete.

TDD offers support in the small for a similar incremental mind-set. You tackle units one by one, defining and verifying them with tests. At any given point, you can stop development and know that you have built everything the tests say the system does. Anything not tested is not implemented, and anything tested is implemented correctly and completely.

Test Behavior, Not Methods

A common mistake for TDD newbies is to focus on testing member functions. “We have an add member function. Let’s write TEST(ARetweetCollection, Add).” But fully covering add behavior requires coding to a few different scenarios. The result is that you must lump a bunch of different behaviors into a single test. The documentation value of the tests diminishes, and the time to understand a single test increases.

Instead, focus on behaviors or cases that describe behaviors. What happens when you add a tweet that you’ve already added before? What if a client passes in an empty tweet? What if the user is no longer a valid Twitter user?

We translate the set of concerns around adding tweets into separate tests.

 
TEST(ARetweetCollection, IgnoresDuplicateTweetAdded)
 
TEST(ARetweetCollection, UsesOriginalTweetTextWhenEmptyTweetAdded)
 
TEST(ARetweetCollection, ThrowsExceptionWhenUserNotValidForAddedTweet)

As a result, you can holistically look at the test names and know exactly the behaviors that the system supports.

Using Tests to Describe Behavior

Think of your tests as examples that describe, or document, the behavior of your system. The full understanding of a well-written test is best gleaned by combining two things: the test name, which summarizes the behavior exhibited given a specific context, and the test statements themselves, which demonstrate the summarized behavior for a single example.

c3/11/RetweetCollectionTest.cpp
 
TEST_F(ARetweetCollection, IgnoresDuplicateTweetAdded) {
 
Tweet tweet(​"msg"​, ​"@user"​);
 
Tweet duplicate(tweet);
 
collection.add(tweet);
 
 
collection.add(duplicate);
 
 
ASSERT_THAT(collection.size(), Eq(1u));
 
}

The test name provides a high-level summary: a duplicate tweet added should be ignored. But what makes a tweet a duplicate, and what does it mean to ignore a tweet? The test provides a simple example that makes the answers to those two questions clear in a matter of seconds. A duplicate tweet is an exact copy of another, and the size remains unchanged after the duplicate is added.

The more you think about TDD as documentation, the more you understand the importance of a high-quality test. The documentation aspect of the tests is a by-product of doing TDD. To ensure your investment in unit tests returns well on value, you must ensure that others can readily understand your tests. Otherwise, your tests will waste their time.

Good tests will save time by acting as a trustworthy body of comprehensive documentation on the behaviors your system exhibits. As long as all your tests pass, they accurately impart what the system does. Your documentation won’t get stale.

Keeping It Simple

The cost of unnecessary complexity never ends. You’ve no doubt wasted countless hours struggling to decipher a complex member function or convoluted design. Most of the time, you could have programmed the solution more simply and saved everyone a lot of time.

Developers create unnecessary complexity for numerous reasons.

  • Time pressure. “We just need to ship this code and move on. We have no time to make it pretty.” Sooner or later, you’ll have no time, period, because it’s taking ten times longer to do anything. Haste allows complexity to grow, which slows you down in so many ways (comprehension, cost of change, build time).

  • Lack of education. You need to want better code, and you need to admit when your code sucks, but that means you must know the difference. Seek honest feedback from teammates through pairing and other review forms. Learn how to recognize design deficiencies and code smells. Learn how to correct them with better approaches—take advantage of the many great books on creating clean designs and code.

  • Existing complexity. A convoluted legacy codebase will make you code through hoops to add new behavior. Long methods promote longer methods, and tightly coupled designs promote even more coupling.

  • Fear of changing code. Without fast tests, you don’t always do the right things in code. “If it ain’t broke, don’t fix it.” Fear inhibits proper refactoring toward a good, sustainable design. With TDD, every green bar you get represents an opportunity to improve the codebase (or at least prevent it from degrading). Chapter 6, Incremental Design is dedicated to doing the right things in code.

  • Speculation. “We’re probably going to have a many-to-many relationship between customers and accounts down the road, so let’s just build that in now.” Maybe you’ll be right most of the time, but you’ll live with premature complexity in the meantime. Sometimes you’ll travel a different road entirely and end up paying for the (unused) additional complexity forever, or at least paying dearly to undo the wrong complexity. Instead, wait until you really need it. It usually won’t cost anything more.

Simplicity is how you survive in a continually changing environment. In an Agile shop, you deliver new features each iteration, some of which you might never have considered before. That’s a challenge; you’ll find yourself force-fitting new features if the existing system can’t accommodate change. Your best defense is a simple design: code that is readable, code that doesn’t exhibit duplication, and code that eschews other unnecessary complexities. These characteristics maximally reduce your maintenance costs.

Sticking to the Cycle

Not following red-green-refactor will cost you. See Getting Green on Red for many reasons why it’s important to first observe a red bar. Obviously, not getting a green when you want one means you’re not adding code that works. More importantly, not taking advantage of the refactoring step of the cycle means that your design will degrade. Without following a disciplined approach to TDD, you will slow down.

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

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