TDD,单元测试和架构变更

4

我正在使用C ++编写RPC中间件。 我有一个名为RPCClientProxy的类,其中包含一个套接字客户端:

class RPCClientProxy {
  ...
  private:
    Socket* pSocket;
  ...
}

构造函数:
RPCClientProxy::RPCClientProxy(host, port) {
  pSocket = new Socket(host, port);
}

正如您所看到的,我不需要告诉用户我有一个套接字。

但是,为了对我的代理进行单元测试,需要创建套接字的模拟,并将其传递给代理。为此,我必须使用setter或在代理的构造函数中传递工厂给套接字。

我的问题是:根据TDD,仅因为测试而这样做是否可行?正如您所看到的,这些更改将改变程序员使用库的方式。


正如你所看到的,在你的问题上可能没有“正确”的答案,你将不得不权衡利弊并做出决定... - Harald Scheirich
4个回答

3
您所描述的情况是完全正常的,有一些已经建立的模式可以帮助您以不影响生产代码的方式实现测试。解决这个问题的一种方法是使用测试特定子类,在其中添加一个setter函数来设置socket成员,并在测试情况下使用mock socket。当然,您需要将变量从私有变量更改为受保护变量,但这可能并不是什么大问题。例如:
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网站的其余部分:有许多成熟的单元测试模式需要了解,希望您能从那些已经走过这条路的人们的知识中获益 :)

这可能是最好的选择。 - Gutzofter
如果RPCClientProxy中没有默认构造函数,那么这是行不通的。 - Edward Strange
@Noah Roberts:不确定它是否行得通。这只是一个需要根据实际情况进行调整的示例!您仍然可以拥有一个特定于测试的子类,并实现添加额外参数的构造函数,因此这个想法没有任何问题。 - jkp
这是一个有趣的方法,我可能会用到它,但这取决于至少有一个不需要做很多工作的构造函数。在这种情况下,根据Socket的构造函数所做的操作,当您调用(host,port)构造函数时,系统可能已经失败。因此,在完美的世界中,我会说Matthew的答案应该优先考虑,如果必须使用,则可以使用此答案。 - Edward Strange
那么这里最好的选择是将必要的构造函数实现为私有的,并在“friend”可测试类中使用它们,对吗?我还需要创建接口来模拟对象。 - Leandro

3

我不遵循特定的规范,如果您认为通过模拟套接字进行测试会有所收益,那么请尝试。您可以实现一个并行构造函数。

RPCClientProxy::RPCClientProxy(Socket* socket) 
{
   pSocket = socket
}

另一种选择是实现一个主机来进行连接测试,您可以配置它来期望特定的消息。


3

您的问题更多是一个设计问题。

如果你想实现另一种Socket的行为,那么你就需要重写创建socket的所有代码,这非常麻烦。

通常的想法是使用抽象基类(接口)Socket,然后根据情况使用抽象工厂来创建所需的socket。工厂本身可以是单例(虽然我更喜欢单子型),或者作为参数传递下来(根据依赖注入的原则)。请注意,后者意味着没有全局变量,当然对于测试来说更好。

因此,我建议采取以下措施:

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表达的清晰资源所有权。

我喜欢工厂,但是套接字不是我想要模拟的中间件的唯一组件。在RPCClientProxy中,我有:RPCCallHash:当客户端发送请求时,我需要在哈希表中放置一个表示正在运行的调用的条目;RPCCallQueue:当服务器返回响应时,中间件将排队一个响应对象。vector<RPCCallHandler*>:它们是负责出列响应并调用某些回调函数的线程。线程数是可配置的。那么,你认为呢?是否将所有我想要模拟的中间件组件都制作成工厂是个好主意? - Leandro
只是一点提醒。在这种情况下,您实际上并没有被烤焦。假设您想要创建新抽象的点恰好是客户端正在创建的对象,则可以将任何构造函数转换为抽象工厂。请参见http://stackoverflow.com/questions/2884814/is-using-a-c-virtual-constructor-generally-considered-good-practice/2884907#2884907。 - Edward Strange
此外,unique_ptr是C++0x的一部分。除此之外,这当然是“正确”的答案。 - Edward Strange
@Noah:确切地说,您可以使用Handle/Body模式来移动使用工厂,但是这需要一个“单例”工厂,其中存在在多线程环境中使用全局变量的所有问题。 - Matthieu M.
@Leandro:这就是依赖注入的问题,如果你想正确地做到它,最终你会传递很多项。虽然你可以将它们概括成一个块,以便只传递一个对象。 - Matthieu M.
显示剩余2条评论

0

正如其他人所指出的,在这种情况下,工厂架构或测试特定的子类都是不错的选择。为了完整起见,还有另一种可能性是使用默认参数:

RGCClientProxy::RPCClientProxy(Socket *socket = NULL)
{
    if(socket == NULL) {
        socket = new Socket();
    }
    //...
}

这种方式介于工厂模式(最灵活但对用户来说更痛苦)和在构造函数中新建套接字之间。它的好处是现有的客户端代码不需要修改。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接