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