Что плохого в том, чтобы сделать юнит-тест другом класса, который он тестирует?

В C++ я часто делал класс модульных тестов другом класса, который я тестирую. Я делаю это потому, что иногда чувствую необходимость написать модульный тест для закрытого метода или, возможно, мне нужен доступ к какому-либо приватному члену, чтобы мне было проще настроить состояние объекта и проверить его. Для меня это помогает сохранить инкапсуляцию и абстракцию, потому что я не изменяю открытый или защищенный интерфейс класса.

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

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

Вот почему я говорю, что это сохраняет абстракцию и инкапсуляцию.

На моей новой работе они не одобряют использование классов друзей даже для юнит-тестов. Они говорят, потому что класс не должен ничего "знать" о тестах, и что вы не хотите тесной связи класса и его теста.

Может кто-нибудь объяснить мне эти причины больше, чтобы я мог лучше понять? Я просто не понимаю, почему использовать друга для юнит-тестов плохо.

7 ответов

Решение

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

Опять же, это идеальный случай. В реальном мире все не всегда так ясно, и если класс юнит-тестирования является другом класса, который он тестирует, он может быть приемлемым или даже желательным. Тем не менее, это, вероятно, не то, что вы хотите делать все время. Если это происходит достаточно часто, это может быть признаком того, что ваши классы слишком большие и / или выполняют слишком много задач. Если это так, дальнейшее разделение их путем рефакторинга сложных наборов частных методов в отдельные классы должно помочь устранить необходимость в модульных тестах, чтобы знать о деталях реализации.

Я чувствую, что Bcat дал очень хороший ответ, но я хотел бы остановиться на исключительном случае, на который он ссылается

В реальном мире все не всегда так ясно, и если класс юнит-тестирования является другом класса, который он тестирует, он может быть приемлемым или даже желательным.

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

  • Мы страдаем от неприлично больших функций и классов, которые требуют рефакторинга, но для рефакторинга полезно иметь тесты.
  • Большая часть нашего кода зависит от доступа к базе данных, которая по разным причинам не должна вводиться в модульные тесты.

В некоторых случаях Mocking полезен для смягчения последней проблемы, но очень часто это просто приводит к неоправданно сложному дизайну (классовые иерархии, где в противном случае не потребуется), в то время как можно очень просто выполнить рефакторинг кода следующим образом:

class Foo{
public:
     some_db_accessing_method(){
         // some line(s) of code with db dependance.

         // a bunch of code which is the real meat of the function

         // maybe a little more db access.
     }
}

Теперь у нас есть ситуация, когда ядро ​​функции нуждается в рефакторинге, поэтому мы хотели бы провести модульное тестирование. Это не должно быть обнародовано. Теперь, есть замечательная техника, называемая насмешкой, которая может быть использована в этой ситуации, но факт в том, что в этом случае издевательство является излишним. Это потребовало бы от меня увеличения сложности конструкции из-за ненужной иерархии.

Гораздо более прагматичным подходом было бы сделать что-то вроде этого:

 class Foo{
 public: 
     some_db_accessing_method(){
         // db code as before
         unit_testable_meat(data_we_got_from_db);
         // maybe more db code.    
 }
 private:
     unit_testable_meat(...);
 }

Последнее дает мне все преимущества, которые мне нужны от модульного тестирования, в том числе дает мне ценную страховочную сеть, чтобы ловить ошибки, возникающие при рефакторинге кода в мясе. Чтобы выполнить его модульное тестирование, я должен подружиться с классом UnitTest, но я бы настоятельно утверждал, что это гораздо лучше, чем бесполезная в противном случае кодовая иерархия, просто позволяющая мне использовать Mock.

Я думаю, что это должно стать идиомой, и я думаю, что это подходящее, прагматичное решение для увеличения ROI модульного тестирования.

Вам следует учитывать, что существуют различные стили и методы для тестирования: Тестирование черного ящика только проверяет открытый интерфейс (рассматривая класс как черный ящик). Если у вас есть абстрактный базовый класс, вы можете использовать одни и те же тесты для всех ваших реализаций.

Если вы используете тестирование белого ящика, вы можете даже взглянуть на детали реализации. Не только о том, какие частные методы есть в классе, но и о том, какие условные операторы включены (например, если вы хотите увеличить охват условий, потому что знаете, что условия было трудно кодировать). В тестировании белого ящика вы определенно имеете "высокую связь" между классами / реализацией и тестами, что необходимо, потому что вы хотите протестировать реализацию, а не интерфейс.

Как указал bcat, часто полезно использовать состав и больше, но меньшие классы вместо многих частных методов. Это упрощает тестирование "белого ящика", потому что вы можете легче указать контрольные примеры, чтобы получить хорошее покрытие тестами.

Как и предположил bcat, по мере возможности вам нужно находить ошибки, используя сам открытый интерфейс. Но если вы хотите сделать такие вещи, как печать личных переменных и сравнение с ожидаемым результатом и т. Д. (Полезно для разработчиков, чтобы легко отлаживать проблемы), то вы можете сделать класс UnitTest дружественным классу для тестирования. Но вам может понадобиться добавить его под макрос, как показано ниже.

class Myclass
{
#if defined(UNIT_TEST)
    friend class UnitTest;
#endif
};

Включайте флаг UNIT_TEST только тогда, когда требуется модульное тестирование. Для других выпусков вам необходимо отключить этот флаг.

Во многих случаях я не вижу ничего плохого в использовании класса юнит-тестирования друга. Да, разложение большого класса на более мелкие иногда является лучшим способом. Я думаю, что люди слишком поспешно отказываются от использования ключевого слова friend для чего-то подобного - это может быть не идеальный объектно-ориентированный дизайн, но я могу пожертвовать небольшим идеализмом для лучшего освещения тестов, если это то, что мне действительно нужно.

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

Сделайте функции, которые вы хотите проверить защищенными. Теперь в вашем модульном тестовом файле создайте производный класс. Создайте общедоступные функции-обертки, которые будут вызывать ваши защищенные функции тестируемого класса

Другие вопросы по тегам