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.
3.137.216.175