Writing Assertions First

As you interact more with other TDD practitioners, you’ll find there seem to be as many ways to approach TDD as there are practitioners. Consider yourself fortunate if you manage to avoid vociferous debates about the One True Way. For example, you’ll find developers who heavily promote One Assert per Test and others who think it’s an overblown goal. You’ll find practitioners who insist that the test’s name be in an exact format and many others who don’t worry about the name at all.

Is there a right or a wrong? Most of the time, you’ll find proponents on both sides of each argument offering solid rationale behind their preference. Throughout this book, you’ll have noted my style, and no doubt there are things you find more appealing than others. My recommendation is to try the things you find alien or disagree with before dismissing them out of hand. You just might uncover a pleasant surprise. I long ago balked at One Assert per Test (see One Assert per Test) and now find value 99 percent of the time in adhering to it.

Ultimately, beyond following the TDD cycle and producing high-quality code, everything else is a matter of style and preference. Remember, however, that it’s your duty as a professional to seek better ways of doing things. Regardless of how you feel about my style or yours, I hope we don’t find each other working the same way two years from now.

Assert-Act-Arrange?

The order of steps in TDD’s red-green-refactor cycle isn’t negotiable, but the order in which you code individual statements within a test is. Many developers work top-down. They start by coding the Arrange portion of the test, move on to the Act statement, and finally Assert the results. There’s nothing wrong with that approach (it happens to be the way I usually work), but a potentially better approach is to write the assertion first.

By now, you’re used to designing test code against yet-to-be-written production code. The notion of writing an assert against nonexistent test code shouldn’t be too shocking. But why would you want to?

Writing the assertion first makes you think about the goal of the behavior you’re adding. It further forces you to describe what it means for the goal to have been achieved. If this is a struggle, perhaps you don’t have enough information yet to continue writing the test.

More importantly, writing assertions first will grow your use of programming by intention, which should result in clearer tests. Your assertions will be declarations of intent. In contrast, if you’ve already written Arrange and Act, your assertion is more likely to be an implementation-specific detail.

Examples First, or Second at Least

Let’s run through a quick example. We need a test for the GeoServer that returns an empty location when a user is no longer tracked.

c9/18/GeoServerTest.cpp
 
TEST(AGeoServer, AnswersUnknownLocationWhenUserNoLongerTracked) {
 
CHECK_TRUE(locationIsUnknown(aUser));
 
}

We know what the outcome need be, so we express it, though we don’t yet know how to implement the code that will verify that outcome. In the test group, we define a function that supplies default, failing behavior.

c9/18/GeoServerTest.cpp
 
TEST_GROUP(AGeoServer) {
 
// ...
*
bool​ locationIsUnknown(​const​ ​string​& user) {
*
return​ false;
*
}
 
};

After verifying that the test fails, we move on to defining the action.

c9/19/GeoServerTest.cpp
 
TEST(AGeoServer, AnswersUnknownLocationWhenUserNoLongerTracked) {
*
server.stopTracking(aUser);
 
 
CHECK_TRUE(locationIsUnknown(aUser));
 
}

Letting the name of our test be our guide, we provide an arrangement.

c9/20/GeoServerTest.cpp
 
TEST(AGeoServer, AnswersUnknownLocationWhenUserNoLongerTracked) {
*
server.track(aUser);
 
 
server.stopTracking(aUser);
 
 
CHECK_TRUE(locationIsUnknown(aUser));
 
}

And finally, letting failure be our guide, we provide an implementation for the intention-revealing function locationIsUnknown.

c9/20/GeoServerTest.cpp
 
TEST_GROUP(AGeoServer) {
 
// ...
 
*
bool​ locationIsUnknown(​const​ ​string​& user) {
*
auto​ location = server.locationOf(user);
*
*
return​ location.latitude() == numeric_limits<​double​>::infinity();
*
}
 
};

“Well, that’s ugly,” says someone on our team. We add the capability to ask whether a location is the “unknown” location to the Location class itself and change the implementation of locationIsUnknown.

c9/21/GeoServerTest.cpp
 
TEST_GROUP(AGeoServer) {
 
// ...
*
bool​ locationIsUnknown(​const​ ​string​& user) {
*
return​ server.locationOf(user).isUnknown();
*
}
 
};

We immediately recognize that the helper no longer pulls its weight, so we eliminate it entirely.

c9/22/GeoServerTest.cpp
 
TEST(AGeoServer, AnswersUnknownLocationWhenUserNoLongerTracked) {
 
server.track(aUser);
 
 
server.stopTracking(aUser);
 
 
CHECK_TRUE(server.locationOf(aUser).isUnknown());
 
}

Oh! What was the point? Perhaps we could have designed the Location class that way from the get-go and written the single-line assertion immediately.

Maybe, maybe not. What’s important is that the assertion ends up being a simple declaration, not how it got that way. Often you’ll require a few dense, detailed code statements to verify a test. Even if you didn’t start by declaring an intent for your assertion, you’ll want to ensure you extract those few dense lines to an explanatory helper function.

Assertions requiring more than a single-line declaration create additional work for the test reader.

c9/23/GeoServerTest.cpp
 
TEST(AGeoServer, AnswersUnknownLocationWhenUserNoLongerTracked) {
 
server.track(aUser);
 
 
server.stopTracking(aUser);
 
 
// slow reading. Fix this.
 
auto​ location = server.locationOf(aUser);
 
CHECK_EQUAL(numeric_limits<​double​>::infinity(), location.latitude());
 
}

Not only does the reader need to step through two lines of assertion, they need to determine the dependency between them (the expected argument portion of the CHECK statement references the location returned by the prior line). The reader must also mentally piece these two lines together into a single concept (that the location for the user is unknown).

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

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