Getting Test Doubles in Place

You have two jobs when introducing a test double. First, code the test double. Second, get the target to use an instance of it. Certain techniques for doing so are known as dependency injection (DI).

In the PlaceDescriptionService example, we injected the test double via a constructor. In some circumstances, you might find it more appropriate to pass the test double using a setter member function. These ways to inject a test double are known as (surprise!) constructor injection or setter injection.

Other techniques for getting test doubles in place exist. Use the one that’s most appropriate for your circumstance.

Override Factory Method and Override Getter

To apply Override Factory Method, you must change the production code to use a factory method any time a collaborator instance is needed. Here’s one way to implement the change in the PlaceDescriptionService:

c5/15/PlaceDescriptionService.h
 
#include <memory>
 
// ...
 
virtual​ ~PlaceDescriptionService() {}
 
// ...
 
protected​:
 
virtual​ std::shared_ptr<Http> httpService() ​const​;
c5/15/PlaceDescriptionService.cpp
 
#include "CurlHttp.h"
 
string​ PlaceDescriptionService::get(​const​ ​string​& url) ​const​ {
*
auto​ http = httpService();
*
http->initialize();
*
return​ http->get(url);
 
}
 
*
shared_ptr<Http> PlaceDescriptionService::httpService() ​const​ {
*
return​ make_shared<CurlHttp>();
*
}

Instead of referring to the member variable http_ for interactions with the HTTP service, the code now calls the protected member function httpService to obtain an Http pointer.

In the test, we define a derivative of PlaceDescriptionService. The primary job of this subclass is to override the factory method (httpService) that returns an Http instance.

c5/15/PlaceDescriptionServiceTest.cpp
 
class​ PlaceDescriptionService_StubHttpService: ​public​ PlaceDescriptionService {
 
public​:
 
PlaceDescriptionService_StubHttpService(shared_ptr<HttpStub> httpStub)
 
: httpStub_{httpStub} {}
 
shared_ptr<Http> httpService() ​const​ override { ​return​ httpStub_; }
 
shared_ptr<Http> httpStub_;
 
};

We change our tests to create an HttpStub shared pointer and store it in the PlaceDescriptionService_StubHttpService instance. Here’s what MakesHttpRequestToObtainAddress now looks like:

c5/15/PlaceDescriptionServiceTest.cpp
 
TEST_F(APlaceDescriptionService, MakesHttpRequestToObtainAddress) {
 
InSequence forceExpectationOrder;
*
shared_ptr<HttpStub> httpStub{​new​ HttpStub};
 
 
string​ urlStart{
 
"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"​};
 
 
auto​ expectedURL = urlStart +
 
"lat="​ + APlaceDescriptionService::ValidLatitude + ​"&"​ +
 
"lon="​ + APlaceDescriptionService::ValidLongitude;
*
EXPECT_CALL(*httpStub, initialize());
*
EXPECT_CALL(*httpStub, get(expectedURL));
*
PlaceDescriptionService_StubHttpService service{httpStub};
 
 
service.summaryDescription(ValidLatitude, ValidLongitude);
 
}

Override Factory Method demonstrates the hole in coverage created by using test doubles. Since our test overrides the production implementation of httpService, code in that method never gets exercised by the tests. As stated before, make sure you have an integration test that requires use of the real service! Also, don’t let any real logic sneak into the factory method; otherwise, you’ll grow the amount of untested code. The factory method should return only an instance of the collaborator type.

As an alternative to Override Factory Method, you can use Override Getter. With respect to our example, the difference is that the httpServer function is a simple getter that returns a member variable referencing an existing instance, whereas in Override Factory, httpServer is responsible for constructing the instance. The test remains the same.

c5/16/PlaceDescriptionService.h
 
class​ PlaceDescriptionService {
 
public​:
*
PlaceDescriptionService();
 
virtual​ ~PlaceDescriptionService() {}
 
std::​string​ summaryDescription(
 
const​ std::​string​& latitude, ​const​ std::​string​& longitude) ​const​;
 
 
private​:
 
// ...
*
std::shared_ptr<Http> http_;
 
 
protected​:
 
virtual​ std::shared_ptr<Http> httpService() ​const​;
 
};
c5/16/PlaceDescriptionService.cpp
 
PlaceDescriptionService::PlaceDescriptionService()
 
: http_{make_shared<CurlHttp>()} {}
 
// ...
 
shared_ptr<Http> PlaceDescriptionService::httpService() ​const​ {
*
return​ http_;
 
}

Used sparingly, Override Factory Method and Override Getter are simple and effective, particularly in legacy code situations (see Chapter 8, Legacy Challenges). Prefer constructor or setter injection, however.

Introduce via Factory

A factory class is responsible for creating and returning instances. If you have an HttpFactory, you can have your tests tell it to return an HttpStub instance instead of an Http (production) instance. If you don’t already have a legitimate use for a factory, don’t use this technique. Introducing a factory only to support testing is a poor choice.

Here’s our factory implementation:

c5/18/HttpFactory.cpp
 
#include "HttpFactory.h"
 
#include "CurlHttp.h"
 
#include <memory>
 
 
using​ ​namespace​ std;
 
 
HttpFactory::HttpFactory() {
 
reset();
 
}
 
 
shared_ptr<Http> HttpFactory::get() {
 
return​ instance;
 
}
 
 
void​ HttpFactory::reset() {
 
instance = make_shared<CurlHttp>();
 
}
 
 
void​ HttpFactory::setInstance(shared_ptr<Http> newInstance) {
 
instance = newInstance;
 
}

During setup, the test creates a factory and injects an HttpStub instance into it. Subsequent requests to get on the factory return this test double.

c5/18/PlaceDescriptionServiceTest.cpp
 
class​ APlaceDescriptionService: ​public​ Test {
 
public​:
 
static​ ​const​ ​string​ ValidLatitude;
 
static​ ​const​ ​string​ ValidLongitude;
 
 
shared_ptr<HttpStub> httpStub;
 
shared_ptr<HttpFactory> factory;
 
shared_ptr<PlaceDescriptionService> service;
 
 
virtual​ ​void​ SetUp() override {
 
factory = make_shared<HttpFactory>();
 
service = make_shared<PlaceDescriptionService>(factory);
 
}
 
 
void​ TearDown() override {
 
factory.reset();
 
httpStub.reset();
 
}
 
};
 
 
class​ APlaceDescriptionService_WithHttpMock: ​public​ APlaceDescriptionService {
 
public​:
 
void​ SetUp() override {
 
APlaceDescriptionService::SetUp();
 
httpStub = make_shared<HttpStub>();
 
factory->setInstance(httpStub);
 
}
 
};
 
 
TEST_F(APlaceDescriptionService_WithHttpMock, MakesHttpRequestToObtainAddress) {
 
string​ urlStart{
 
"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"​};
 
auto​ expectedURL = urlStart +
 
"lat="​ + APlaceDescriptionService::ValidLatitude + ​"&"​ +
 
"lon="​ + APlaceDescriptionService::ValidLongitude;
 
EXPECT_CALL(*httpStub, initialize());
 
EXPECT_CALL(*httpStub, get(expectedURL));
 
service->summaryDescription(ValidLatitude, ValidLongitude);
 
}

We change the production code in summaryDescription to obtain its Http instance from the factory.

c5/18/PlaceDescriptionService.cpp
 
string​ PlaceDescriptionService::get(​const​ ​string​& url) ​const​ {
*
auto​ http = httpFactory_->get();
 
http->initialize();
 
return​ http->get(url);
 
}

Since we’re passing the factory through the constructor, this pattern is little different from constructor injection, except that we now have an extra layer of indirection.

Introduce via Template Parameter

Some of the injection techniques can be somewhat clever. Injecting via a template parameter is another option that doesn’t require clients to pass a collaborator instance. Its use is best constrained to legacy situations where a template already exists.

We declare the PlaceDescriptionService class as a template that can be bound to a single typename, HTTP. We add a member variable, http_, of the parameter type HTTP. Since we want clients to use the class name PlaceDescriptionService, we rename the template class to PlaceDescriptionServiceTemplate. After the template definition, we supply a typedef that defines the type PlaceDescriptionService as PlaceDescriptionServiceTemplate bound to the production class Http. Here’s the code:

c5/19/PlaceDescriptionService.h
 
template​<​typename​ HTTP>
 
class​ PlaceDescriptionServiceTemplate {
 
public​:
 
// ...
 
// mocks in tests need the reference
 
HTTP& http() {
 
return​ http_;
 
}
 
private​:
 
// ...
 
std::​string​ get(​const​ std::​string​& url) {
 
http_.initialize();
 
return​ http_.get(url);
 
}
 
// ...
 
HTTP http_;
 
};
 
class​ Http;
 
typedef​ PlaceDescriptionServiceTemplate<Http> PlaceDescriptionService;

In the test fixture, we declare the service to be of type PlaceDescriptionServiceTemplate bound to the mock type, HttpStub:

c5/19/PlaceDescriptionServiceTest.cpp
 
class​ APlaceDescriptionService_WithHttpMock: ​public​ APlaceDescriptionService {
 
public​:
 
PlaceDescriptionServiceTemplate<HttpStub> service;
 
};

The test doesn’t provide PlaceDescriptionService with an instance of the mock; it supplies the mock’s type. PlaceDescriptionService creates its own instance of this type (as the member variable http_). Since Google Mock will be looking to verify interaction expectations on the template object’s instance, we need to provide the test with access to it. We change the test to obtain the stub instance via an accessor function on PlaceDescriptionServiceTemplate named http.

c5/19/PlaceDescriptionServiceTest.cpp
 
TEST_F(APlaceDescriptionService_WithHttpMock, MakesHttpRequestToObtainAddress) {
 
 
string​ urlStart{
 
"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"​};
 
 
auto​ expectedURL = urlStart +
 
"lat="​ + APlaceDescriptionService::ValidLatitude + ​"&"​ +
 
"lon="​ + APlaceDescriptionService::ValidLongitude;
*
EXPECT_CALL(service.http(), initialize());
*
EXPECT_CALL(service.http(), get(expectedURL));
 
 
service.summaryDescription(ValidLatitude, ValidLongitude);
 
}

You can support introducing a mock via a template parameter in a number of ways, some even more clever than this implementation (which is based on the Template Redefinition pattern in Working Effectively with Legacy Code [Fea04]).

Injection Tools

Tools to handle injecting collaborators as dependent objects are known as dependency injection (DI) tools. Two known C++ examples are Autumn Framework[26] and Qt IoC Container.[27] Michael Feathers weighs in on the state of DI frameworks in C++: “It seems that to do DI in C++, you have to place constraints on the classes you create...you have to make them inherit from some other class, use macro preregistration or a metaobject library.”[28] You should first master the manual injection techniques described here and then investigate the tools to see whether they improve things. DI tools are generally much more effective in languages that support full reflective capabilities.

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

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