【问题标题】:TDD, Unit Test and architectural changesTDD、单元测试和架构变更
【发布时间】:2010-05-25 14:24:48
【问题描述】:

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

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

构造函数:

RPCClientProxy::RPCClientProxy(host, port) {
  pSocket = new Socket(host, port);
}

如你所见,我不需要告诉用户我里面有一个套接字。

虽然,要为我的代理进行单元测试,有必要为套接字创建模拟并将它们传递给代理,为此我必须使用 setter 或将工厂传递给代理构造函数中的套接字。

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

【问题讨论】:

  • 如您所见,您的问题可能没有“正确”答案,您必须权衡利弊并下定决心......

标签: c++ unit-testing tdd


【解决方案1】:

我不遵守某个规范,我会说,如果您认为通过模拟套接字进行测试会受益,那么您可以实现并行构造函数

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

另一种选择是实现一个主机来连接以进行测试,您可以将其配置为预期某些消息

【讨论】:

    【解决方案2】:

    您所描述的是一种完全正常的情况,并且有一些既定的模式可以帮助您以不会影响生产代码的方式实施测试。

    解决此问题的一种方法是使用Test Specific Subclass,您可以在其中为套接字成员添加一个设置器,并在测试的情况下使用模拟套接字。当然,您需要使变量受保护而不是私有,但这可能没什么大不了的。例如:

    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 网站的其余部分:有一大堆完善的单元测试模式需要理解,希望你能从那些去过那里的人那里获得知识在你之前:)

    【讨论】:

    • 这可能是最好的路线
    • 如果 RPCClientProxy 中没有默认构造函数,那就不行了。
    • @Noah Roberts:不确定它不会。这是一个可以根据需要进行调整的示例!您仍然可以拥有一个特定于测试的子类并实现一个添加额外参数的构造函数,因此这个想法没有任何问题。
    • 这是一种有趣的方法,我可能会使用它,但它确实取决于至少可以访问一个不做很多工作的构造函数。在这种情况下,根据 Socket 的构造函数所做的事情,当您调用 (host, port) 构造函数时,系统可能已经失败。因此,我想说马修的答案在完美的世界中应该是首选的,如果必须的话,尽可能使用这个答案。
    • 所以这里最好的选择是将必要的构造函数实现为私有并在“朋友”可测试类中使用它们,对吗?而且我还需要创建和连接可模拟对象。
    【解决方案3】:

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

    如果您曾经为 Socket 实现其他行为,您会大吃一惊,因为它涉及重写所有创建套接字的代码。

    通常的想法是使用抽象基类(接口)Socket,然后根据情况使用抽象工厂创建您想要的套接字。工厂本身可以是单例(尽管我更喜欢 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 表达的明确资源所有权。

    【讨论】:

    • 我喜欢工厂,但套接字不是我想模拟的中间件的唯一组件。在 RPCClientProxy 中,我有: RPCCallQueue:当服务器返回响应时,中间件将响应对象排入队列。 vector:它们是负责将响应出列并调用一些回调函数的线程。线程数是可配置的。那么,你怎么看?从我想模拟的中间件中为所有组件制作工厂是个好主意吗?
    • 只是一个注释。在这种情况下,您实际上并没有被敬酒。您可以将任何构造函数转换为抽象工厂,假设您要创建新抽象的点正是客户端正在创建的对象。见stackoverflow.com/questions/2884814/…
    • 另外,unique_ptr 是 C++0x。除此之外,这当然是“正确”的答案。
    • @Noah:确切地说,您可以使用 Handle/Body 习语来移动使用工厂,但这需要一个“单例”工厂,其中存在在多线程环境中使用全局变量的所有问题。
    • @Leandro:这就是依赖注入的问题,如果你想正确地做到这一点,你最终会传递很多项目。尽管您可以想象将它们分组为一个 blob,以便只传递一个对象。
    【解决方案4】:

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

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

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

    【讨论】:

      猜你喜欢
      • 2023-03-05
      • 1970-01-01
      • 2011-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-26
      • 2014-01-23
      相关资源
      最近更新 更多