Предположим что имеется некая функция add со странной логикой:
(Весь код ниже написан на выдуманном псевдокоде)
int sum(int a, int b) {
if (a == 2 && b == 2) return 5;
return a + b;
}
Юнит тесты
При модульном(или юнит) тестировании мы полагаемся на сакральные знания имплементации и валидируем что мы правильно понимаем суть написанного. Определяем все бранчи(куда может завернуть нас то, или иное исполнение) и на каждый из бранчей составляем тест на соответствие:
Конкретные входные данные(или некое множество входных данных) на выходе дадут вполне конкретное значение. Напишем 2 юнит-теста:
test_first_case() {
sum(2, 2) equals 5
}
test_second_case() {
sum(4, 10) equals 14
}
Очевидно, что эти тесты очень чуствительны к имплементации, ведь нетрудно убедиться, что если мы изменим какие-либо проверки, то один или более тестов упадут. Все верно, они позволяют нам проверить что выполняется именно тот код, который мы нарисовали у себя в голове. И что транзисторы делают именно то, что нам хочется.
Функциональные тесты
Мы получили ответ на вопрос "Делает ли мой код, то, что я думаю", но теперь мы хотим получить ответ на вопрос "А делает ли он то, что я хочу?".
Чтобы это проверить необходимо составить дополнительный(дополнительные) тест(тесты) без знания внутренней имплементации. То есть при их составлении оперировать только сигнатурой и документацией:
test_for_additivity() {
for (a in range(MIN_INT, MAX_INT), b in range(MIN_INT, MAX_INT)) {
sum(a, b) equals (a + b)
}
}
Тут-то можно четко понять разницу между модульным и функциональным тестированием.
(Продолжение следует)