A Hand-Crafted Test Double

To use a test double, you must be able to supplant the behavior of the CurlHttp class. C++ provides many different ways, but the predominant manner is to take advantage of polymorphism. Let’s take a look at the Http interface that the CurlHttp class implements (realizes):

c5/1/Http.h
 
virtual​ ~Http() {}
 
virtual​ ​void​ initialize() = 0;
 
virtual​ std::​string​ get(​const​ std::​string​& url) ​const​ = 0;

Your solution is to override the virtual methods on a derived class, provide special behavior to support testing in the override, and pass the Place Description Service code a base class pointer.

Let’s see some code.

c5/1/PlaceDescriptionServiceTest.cpp
 
TEST_F(APlaceDescriptionService, ReturnsDescriptionForValidLocation) {
 
HttpStub httpStub;
 
PlaceDescriptionService service{&httpStub};
 
 
auto​ description = service.summaryDescription(ValidLatitude, ValidLongitude);
 
 
ASSERT_THAT(description, Eq(​"Drury Ln, Fountain, CO, US"​));
 
}

We create an instance of HttpStub in the test. HttpStub is the type of our test double, a class that derives from Http. We define HttpStub directly in the test file so that we can readily see the test double’s behavior along with the tests that use it.

c5/1/PlaceDescriptionServiceTest.cpp
 
class​ HttpStub: ​public​ Http {
 
void​ initialize() override {}
 
std::​string​ get(​const​ std::​string​& url) ​const​ override {
 
return​ ​"???"​;
 
}
 
};

Returning a string with question marks is of little use. What do we need to return from get? Since the external Nominatim Search Service returns a JSON response, we should return an appropriate JSON response that will generate the description expected in our test’s assertion.

c5/2/PlaceDescriptionServiceTest.cpp
 
class​ HttpStub: ​public​ Http {
 
void​ initialize() override {}
 
std::​string​ get(​const​ std::​string​& url) ​const​ override {
 
return​ R​"({ "​address​": {
 
"​road​":"​Drury Ln​",
 
"​city​":"​Fountain​",
 
"​state​":"​CO​",
 
"​country​":"​US​" }})"​;
 
}
 
};

How did I come up with that JSON? I ran a live GET request using my browser (the Nominatim Search Service API page shows you how) and captured the resulting output.

From the test, we inject our HttpStub instance into a PlaceDescriptionService object via its constructor. We’re changing our design from what we speculated. Instead of the service constructing its own Http instance, the client of the service will now need to construct the instance and inject it into (pass it to) the service. The service constructor holds on to the instance via a base class pointer.

c5/2/PlaceDescriptionService.cpp
 
PlaceDescriptionService::PlaceDescriptionService(Http* http) : http_(http) {}

Simple polymorphism gives us the test double magic we need. A PlaceDescriptionService object knows not whether it holds a production Http instance or an instance designed solely for testing.

Once we get our test to compile and fail, we code summaryDescription.

c5/2/PlaceDescriptionService.cpp
 
string​ PlaceDescriptionService::summaryDescription(
 
const​ ​string​& latitude, ​const​ ​string​& longitude) ​const​ {
 
auto​ getRequestUrl = ​""​;
 
auto​ jsonResponse = http_->get(getRequestUrl);
 
 
AddressExtractor extractor;
 
auto​ address = extractor.addressFrom(jsonResponse);
 
return​ address.road + ​", "​ + address.city + ​", "​ +
 
address.state + ​", "​ + address.country;
 
}

(We’re fortunate: someone else has already built AddressExtractor for us. It parses a JSON response and populates an Address struct.)

When the test invokes summaryDescription, the call to the Http method get is received by the HttpStub instance. The result is that get returns our hard-coded JSON string. A test double that returns a hard-coded value is a stub. You can similarly refer to the get method as a stub method.

We test-drove the relevant code into summaryDescription. But what about the request URL? When the code you’re testing interacts with a collaborator, you want to make sure that you pass the correct elements to it. How do we know that we pass a legitimate URL to the Http instance?

In fact, we passed an empty string to the get function in order to make incremental progress. We need to drive in the code necessary to populate getRequestUrl correctly. We could triangulate and assert against a second location (see Triangulation).

Better, we can add an assertion to the get stub method we defined on HttpStub.

c5/3/PlaceDescriptionServiceTest.cpp
 
class​ HttpStub: ​public​ Http {
 
void​ initialize() override {}
 
std::​string​ get(​const​ std::​string​& url) ​const​ override {
*
verify(url);
 
return​ R​"({ "​address​": {
 
"​road​":"​Drury Ln​",
 
"​city​":"​Fountain​",
 
"​state​":"​CO​",
 
"​country​":"​US​" }})"​;
 
}
 
void​ verify(​const​ ​string​& url) ​const​ {
 
auto​ expectedArgs(
 
"lat="​ + APlaceDescriptionService::ValidLatitude + ​"&"​ +
 
"lon="​ + APlaceDescriptionService::ValidLongitude);
 
ASSERT_THAT(url, EndsWith(expectedArgs));
 
}
 
};

(Why did we create a separate method, verify, for our assertion logic? It’s because of a Google Mock limitation: you can use assertions that cause fatal failures only in functions with void return.[18])

Now, when get gets called, the stub implementation ensures the parameters are as expected. The stub’s assertion tests the most important aspect of the URL: does it contain correct latitude/longitude arguments? Currently it fails, since we pass get an empty string. Let’s make it pass.

c5/3/PlaceDescriptionService.cpp
 
string​ PlaceDescriptionService::summaryDescription(
 
const​ ​string​& latitude, ​const​ ​string​& longitude) ​const​ {
*
auto​ getRequestUrl = ​"lat="​ + latitude + ​"&lon="​ + longitude;
 
auto​ jsonResponse = http_->get(getRequestUrl);
 
// ...
 
}

Our URL won’t quite work, since it specifies no server or document. We bolster our verify function to supply the full URL before passing it to get.

c5/4/PlaceDescriptionServiceTest.cpp
 
void​ verify(​const​ ​string​& url) ​const​ {
*
string​ urlStart(
*
"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"​);
*
string​ expected(urlStart +
 
"lat="​ + APlaceDescriptionService::ValidLatitude + ​"&"​ +
 
"lon="​ + APlaceDescriptionService::ValidLongitude);
*
ASSERT_THAT(url, Eq(expected));
 
}

Once we get the test to pass, we undertake a bit of refactoring. Our summaryDescription method violates cohesion, and the way we construct key-value pairs in both the test and production code exhibits duplication.

c5/4/PlaceDescriptionService.cpp
 
string​ PlaceDescriptionService::summaryDescription(
 
const​ ​string​& latitude, ​const​ ​string​& longitude) ​const​ {
 
auto​ request = createGetRequestUrl(latitude, longitude);
 
auto​ response = get(request);
 
return​ summaryDescription(response);
 
}
 
string​ PlaceDescriptionService::summaryDescription(
 
const​ ​string​& response) ​const​ {
 
AddressExtractor extractor;
 
auto​ address = extractor.addressFrom(response);
 
return​ address.summaryDescription();
 
}
 
 
string​ PlaceDescriptionService::get(​const​ ​string​& requestUrl) ​const​ {
 
return​ http_->get(requestUrl);
 
}
 
 
string​ PlaceDescriptionService::createGetRequestUrl(
 
const​ ​string​& latitude, ​const​ ​string​& longitude) ​const​ {
 
string​ server{​"http://open.mapquestapi.com/"​};
 
string​ document{​"nominatim/v1/reverse"​};
 
return​ server + document + ​"?"​ +
 
keyValue(​"format"​, ​"json"​) + ​"&"​ +
 
keyValue(​"lat"​, latitude) + ​"&"​ +
 
keyValue(​"lon"​, longitude);
 
}
 
string​ PlaceDescriptionService::keyValue(
 
const​ ​string​& key, ​const​ ​string​& value) ​const​ {
 
return​ key + ​"="​ + value;
 
}

What about all that other duplication? (“What duplication?” you ask.) The text expressed in the test matches the text expressed in the production code. Should we strive to eliminate this duplication? There are several approaches we might take; for further discussion, refer to Implicit Meaning.

Otherwise, our production code design appears sufficient for the time being. Functions are composed and expressive. As a side effect, we’re poised for change. The function keyValue appears ripe for reuse. We can also sense that generalizing our design to support a second service would be a quick increment, since we’d be able to reuse some of the structure in PlaceDescriptionService.

Our test’s design is insufficient, however. For programmers not involved in its creation, it is too difficult to follow. Read on.

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

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