开发者

TDD, Unit Test and architectural changes

I'm writing an RPC 开发者_开发知识库middleware in C++. I have a class named RPCClientProxy that contains a socket client inside:

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

The constructor:

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

As you can see, I don't need to tell the user that I have a socket inside.

Although, to make unit tests for my proxies it would be necessary to create mocks for sockets and pass them to the proxies, and to do so I must use a setter or pass a factory to the sockets in the proxies's constructors.

My question: According to TDD, is it acceptable to do it ONLY because the tests? As you can see, these changes would change the way the library is used by a programmer.


I don't adhere to a certain canon i would say if you think you would benefit from testing through a mock socket the do it, you could implement a parallel constructor

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

Another option would be to implement a host to connect to for testing that you can configure to expect certain messages


What you describe is a perfectly normal situation, and there are established patterns that can help you implement your tests in a way that won't affect your production code.

One way to solve this is to use a Test Specific Subclass where you could add a setter for the socket member and use a mock socket in the case of a test. Of-course you would need to make the variable protected rather than private but that's probably no biggie. For example:

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;
}

In the end it comes down to the fact that you often (more often than not in C++) have to design your code in such a way as to make it testable and there is nothing wrong with that. If you can avoid these decisions leaking out into the public interfaces that may be better sometimes, but in other cases it can be better to choose, for example, dependency inject through constructor parameters above say, using a singleton to provide access to a specific instance.

Side note: It's probably worth taking a look through the rest of the xunitpatterns.com site: there are a whole load of well established unit-testing patterns to understand and hopefully you can gain from the knowledge of those who have been there before you :)


Your issue is more a problem of design.

If you ever with to implement another behavior for Socket, you're toasted, as it involves rewriting all the code that created sockets.

The usual idea is to use an abstract base class (interface) Socket and then use an Abstract Factory to create the socket you wish depending on the circumstances. The factory itself could be either a Singleton (though I prefer Monoid) or passed down as arguments (according to the tenants of Dependency Injection). Note that the latter means no global variable, which is much better for testing, of course.

So I would advise something along the lines of:

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);
}

It has an impact on the client: you no longer hide the fact that you use sockets behind the scenes. On the other hand, it gives control to the client who may wish to develop some custom sockets (to log, to trigger actions, etc..)

So it IS a design change, but it is not caused by TDD itself. TDD just takes advantage of the higher degree of control.

Also note the clear resource ownership expressed by the use of unique_ptr.


As others have pointed out, a factory architecture or a test-specific subclass are both good options in this situation. For completeness, one other possibility is to use a default argument:

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

This is, perhaps somewhere between the factory paradigm (which is ultimately the most flexible, but more painful for the user) and newing up a socket inside your constructor. It has the benefit that existing client code doesn't need to be modified.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜