Improving Test Abstraction When Using Test Doubles

It’s easy to craft tests that are difficult for others to read. When using test doubles, it’s even easier to craft tests that obscure information critical to their understanding.

ReturnsDescriptionForValidLocation is difficult to understand because it hides relevant information, violating the concept of test abstraction (see Test Abstraction).

c5/4/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"​));
 
}

Why do we expect the description to be an address in Fountain, Colorado? Readers must poke around to discover that the expected address correlates to the JSON address in the HttpStub implementation.

We must refactor the test so that stands on its own. We can change the implementation of HttpStub so that the test is responsible for setting up the return value of its get method.

c5/5/PlaceDescriptionServiceTest.cpp
 
class​ HttpStub: ​public​ Http {
*
public​:
*
string​ returnResponse;
 
void​ initialize() override {}
 
std::​string​ get(​const​ std::​string​& url) ​const​ override {
 
verify(url);
*
return​ returnResponse;
 
}
 
 
void​ verify(​const​ ​string​& url) ​const​ {
 
// ...
 
}
 
};
 
 
TEST_F(APlaceDescriptionService, ReturnsDescriptionForValidLocation) {
 
HttpStub httpStub;
*
httpStub.returnResponse = R​"({"​address​": {
*
"​road​":"​Drury Ln​",
*
"​city​":"​Fountain​",
*
"​state​":"​CO​",
*
"​country​":"​US​" }})"​;
 
PlaceDescriptionService service{&httpStub};
 
auto​ description = service.summaryDescription(ValidLatitude, ValidLongitude);
 
ASSERT_THAT(description, Eq(​"Drury Ln, Fountain, CO, US"​));
 
}

Now the test reader can correlate the summary description to the JSON object returned by HttpStub.

We can similarly move the URL verification to the test.

c5/6/PlaceDescriptionServiceTest.cpp
 
class​ HttpStub: ​public​ Http {
 
public​:
 
string​ returnResponse;
*
string​ expectedURL;
 
void​ initialize() override {}
 
std::​string​ get(​const​ std::​string​& url) ​const​ override {
 
verify(url);
 
return​ returnResponse;
 
}
 
void​ verify(​const​ ​string​& url) ​const​ {
*
ASSERT_THAT(url, Eq(expectedURL));
 
}
 
};
 
 
TEST_F(APlaceDescriptionService, ReturnsDescriptionForValidLocation) {
 
HttpStub httpStub;
 
httpStub.returnResponse = ​// ...
*
string​ urlStart{
*
"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"​};
*
httpStub.expectedURL = urlStart +
*
"lat="​ + APlaceDescriptionService::ValidLatitude + ​"&"​ +
*
"lon="​ + APlaceDescriptionService::ValidLongitude;
 
PlaceDescriptionService service{&httpStub};
 
 
auto​ description = service.summaryDescription(ValidLatitude, ValidLongitude);
 
 
ASSERT_THAT(description, Eq(​"Drury Ln, Fountain, CO, US"​));
 
}

Our test is now a little longer but expresses its intent clearly. In contrast, we pared down HttpStub to a simple little class that captures expectations and values to return. Since it also verifies those expectations, however, HttpStub has evolved from being a stub to becoming a mock. A mock is a test double that captures expectations and self-verifies that those expectations were met.[19] In our example, an HttpStub object verifies that it will be passed an expected URL.

To test-drive a system with dependencies on things such as databases and external service calls, you’ll need several mocks. If they’re only “simple little classes that manage expectations and values to return,” they’ll all start looking the same. Mock tools can reduce some of the duplicate effort required to define test doubles.

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

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