TDD, Unit Test и архитектурные изменения - PullRequest
4 голосов
/ 25 мая 2010

Я пишу промежуточное программное обеспечение RPC на C ++. У меня есть класс с именем RPCClientProxy, который содержит клиент сокета внутри:

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

Конструктор:

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

Как видите, мне не нужно сообщать пользователю, что у меня есть сокет внутри.

Хотя для создания модульных тестов для моих прокси необходимо создать макеты для сокетов и передать их прокси, и для этого я должен использовать установщик или передать фабрику сокетам в конструкторах прокси.

Мой вопрос: согласно TDD, допустимо ли делать это ТОЛЬКО из-за тестов? Как видите, эти изменения изменили бы способ использования библиотеки программистом.

Ответы [ 4 ]

3 голосов
/ 25 мая 2010

Я не придерживаюсь определенного канона, я бы сказал, что если вы думаете, что вам пригодится тестирование через фиктивный сокет, вы можете реализовать параллельный конструктор

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

Другим вариантом будет реализация хоста для подключения к тестированию, которое можно настроить для ожидания определенных сообщений

3 голосов
/ 25 мая 2010

Ваша проблема больше связана с дизайном.

Если вам когда-нибудь понадобится реализовать другое поведение для 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.

3 голосов
/ 25 мая 2010

То, что вы описываете, является совершенно нормальной ситуацией, и существуют установленные шаблоны, которые могут помочь вам реализовать ваши тесты таким образом, чтобы это не повлияло на ваш производственный код.

Одним из способов решения этой проблемы является использование Специфичного для теста подкласса , где вы можете добавить установщик для элемента сокета и использовать фиктивный сокет в случае теста. Конечно, вам нужно сделать переменную защищенной, а не частной, но это, вероятно, не так уж важно. Например:

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 : здесь есть масса хорошо отработанных шаблонов юнит-тестирования, которые можно понять, и мы надеемся, что вы сможете получить их из тех, кто был там до тебя :)

0 голосов
/ 25 мая 2010

Как уже отмечали другие, заводская архитектура или специфичный для теста подкласс являются хорошими вариантами в этой ситуации. Для полноты еще одной возможностью является использование аргумента по умолчанию:

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

Это, возможно, где-то между фабричной парадигмой (которая в конечном итоге является наиболее гибкой, но более болезненной для пользователя) и обновлением сокета внутри вашего конструктора. Преимущество заключается в том, что существующий код клиента не нуждается в изменении.

...