我正在用C ++编写RPC中间件。我有一个名为RPCClientProxy的类,其中包含一个套接字客户端:
class RPCClientProxy {
...
private:
Socket* pSocket;
...
}
构造函数:
RPCClientProxy::RPCClientProxy(host, port) {
pSocket = new Socket(host, port);
}
如您所见,我不需要告诉用户我内部有一个插座。
虽然,为了对我的代理进行单元测试,有必要为套接字创建模拟并将它们传递给代理,为此,我必须使用setter或将工厂传递给代理的构造函数中的套接字。 / p>
我的问题:根据TDD,仅仅因为测试而这样做是否可以接受?如您所见,这些更改将改变程序员使用库的方式。
答案 0 :(得分:3)
您所描述的是完全正常的情况,并且已建立的模式可以帮助您以不会影响生产代码的方式实施测试。
解决此问题的一种方法是使用Test Specific Subclass,您可以在其中为套接字成员添加setter,并在测试时使用模拟套接字。当然,你需要使变量受保护而不是私有,但这可能不是什么大问题。例如:
class RPCClientProxy
{
...
protected:
Socket* pSocket;
...
};
class TestableClientProxy : public RPCClientProxy
{
TestableClientProxy(Socket *pSocket)
{
this->pSocket = pSocket;
}
};
void SomeTest()
{
MockSocket *pMockSocket = new MockSocket(); // or however you do this in your world.
TestableClientProxy proxy(pMockSocket);
....
assert pMockSocket->foo;
}
最后,它归结为这样一个事实:你经常(通常是在C ++中)必须设计你的代码,以使其可测试,并且没有任何问题。如果你可以避免这些决定泄露到公共接口中,有时可能会更好,但在其他情况下,最好选择,例如,依赖注入通过上面的构造函数参数说,使用单例来提供对特定实例的访问
旁注:看看xunitpatterns.com网站的其余部分可能值得一看:有很多完善的单元测试模式可以理解,希望你可以从那些有知识的人那里获益在你面前:)
答案 1 :(得分:3)
我不坚持某个经典我会说如果你认为你会从通过模拟套接字测试中受益,你可以实现并行构造函数
RPCClientProxy::RPCClientProxy(Socket* socket)
{
pSocket = socket
}
另一种选择是实现连接到主机以进行测试,您可以将其配置为期望某些消息
答案 2 :(得分:3)
您的问题更多是设计问题。
如果你曾经为Socket
实现另一种行为,那么你就会被烘烤,因为它涉及重写所有创建套接字的代码。
通常的想法是使用抽象基类(接口)Socket
,然后根据具体情况使用抽象工厂创建所需的套接字。工厂本身可以是Singleton(虽然我更喜欢Monoid)或者作为参数传递(根据依赖注入的租户)。请注意,后者意味着没有全局变量,当然,这对于测试来说要好得多。
所以我会建议:
int main(int argc, char* argv[])
{
SocketsFactoryMock sf;
std::string host, port;
// initialize them
std::unique_ptr<Socket> socket = sf.create(host,port);
RPCClientProxy rpc(socket);
}
它对客户端有影响:您不再隐藏在幕后使用套接字的事实。另一方面,它控制可能希望开发一些自定义套接字的客户端(记录,触发操作等)。
所以它是一个设计变更,但它不是由TDD本身引起的。 TDD只是利用了更高程度的控制。
另请注意使用unique_ptr
表示明确的资源所有权。
答案 3 :(得分:0)
正如其他人所指出的,在这种情况下,工厂架构或特定于测试的子类都是不错的选择。为了完整性,另一种可能性是使用默认参数:
RGCClientProxy::RPCClientProxy(Socket *socket = NULL)
{
if(socket == NULL) {
socket = new Socket();
}
//...
}
这可能介于工厂范例之间(最终是最灵活的,但对用户来说更痛苦)并且在构造函数中新建了一个套接字。它的好处是不需要修改现有的客户端代码。