Предположим, у нас есть функция add()
, как показано ниже:
void add(int a, int b) {
int sum=a+b;
cout<<sum;
sendSumToStorage(sum);
}
Эта простая функция прибавляет к входным значениям, выводит сумму в консоль и также отправляет ее во внешнее хранилище (скажем, в файл). Вот как мы в идеале хотим, чтобы это было в приложении (то есть мы не хотим, чтобы это было вернуть что-нибудь).
Допустимо ли (с точки зрения дизайна) для целей модульного тестирования, если мы изменим сигнатуру функции, чтобы она возвращала sum
? Тогда у нас может быть такой тест:
bool checkAdd() {
int res=add(3, 4);
if (res==7) return true;
else return false;
}
Еще лучше, это (возвращение значения) единственный способ, которым мы могли бы протестировать его? Есть ли способ действительный, с помощью которого мы могли бы протестировать функцию add()
без изменения подписи функции?
@AlanBirtles, не могли бы вы уточнить?
Если вы добавите минимальный воспроизводимый пример, я смогу
Функция, подобная той, что в вашем примере, считается очень плохой практикой.
Почему я говорю это?
Итак, у вас есть метод Добавить, который складывает два числа, а А ТАКЖЕ вызывает что-то еще. По сути, ваш метод делает не одну вещь, а две, что нарушает принцип единой ответственности.
Это значительно усложняет тестирование, потому что вы не можете протестировать только метод add изолированно.
Таким образом, вы бы разделили это на два метода с хорошими именами, которые отражают то, что они делают, и протестировали бы их отдельно.
Если вы не хотите иметь проблем с состоянием между вашими методами, вам придется начать возвращать результаты там, где это имеет смысл.
Я горячо согласен с тем, что вы говорите; но обратите внимание, что приведенное выше только что является примером. Было бы полезнее думать, что у функции есть только две задачи (как у транспортера) — отправить на консоль, а также в хранилище. Выполняемые здесь расчеты — это лишь некоторые параметры конфигурации, связанные с отправкой данных. И именно эти параметры конфигурации мы хотим протестировать.
Игнорируя тот факт, что этот пример имеет плохой дизайн.
В таких случаях, когда вы хотите проверить какое-то внутреннее поведение вместо API, вам следует попробовать использовать некоторые библиотеки тестирования, такие как gtest и gmock. Это позволяет вам описывать более сложные ожидания, чем просто результат функции.
Например, с помощью макроса EXPECT_CALL можно задать ожидание вызова какого-либо метода во время выполнения кода.
Подробнее здесь: https://github.com/google/googletest/blob/master/googlemock/docs/ForDummies.md
Отвечая на ваш вопрос, всегда вредно изменять любую часть тестируемого кода для целей тестирования. В этом случае вы больше не тестируете производственный код. Как было предложено ранее, лучше разделить функциональность на более мелкие части и тестировать их изолированно.
Изменение дизайна кода для улучшения тестируемости очень распространено и обычно считается хорошей практикой. Очевидно, что не все такие изменения обязательно являются реальными улучшениями — иногда существуют лучшие решения.
В вашем случае код сложно протестировать, поскольку он сочетает вычисления (сложение) с взаимодействием с зависимыми компонентами (вывод и сохранение данных). В вашем случае (как указал Андрей) функция также нарушает SRP, но смешивание вычислений и взаимодействий обычно затрудняет тестирование, даже в тех случаях, когда SRP не нарушается.
Я понимаю, вы бы изменили функцию так, чтобы она возвращала вычисленное значение в дополнение к его печати и записи в хранилище. Это позволит вам протестировать вычислительную часть функции. Однако в этом случае функция будет протестирована лишь частично. И цель функции будет дополнительно запутана.
Если вместо этого вы разделите функцию на одну, выполняющую вычисления, и другую, выполняющую взаимодействия, вы можете тщательно протестировать первую с помощью модульного тестирования, а для другой использовать интеграционное тестирование. Опять же, полезность этого подхода не зависит от того, нарушает ли код SRP или нет.
Было бы более нормально как-то наблюдать за значением, отправленным в хранилище, возможно, с помощью фиктивного класса.