История
В конце девяностых – начале нулевых, когда трава была явно зеленее и на конференциях не пришла ещё мода на софт скилы… TDD (test driven development) явился настоящим прорывом в череде завораживающих методологий. Романтика манифеста agile манила красивой жизнью, всё казалось очень экстремальным, разработчики драйвили вовсю и даже задумывались о тестировании, разумеется, о тестировании разработчиками. Ну а тестировщикам досталась целая череда хороших, а иногда даже отличных xUnit фреймворков. По сей день 9 из 10 (по самой скромной оценке) гайдов по автоматизации (и функционального тоже) тестирования построены именно на xUnit – эти инструменты гибкие, открытые, доступные в любом серьезном языке программирования, позволяют из коробки запускать участки кода с зависимостями, параллелизмом, связями и контролем результатов. Есть в бочке меда ложка дегтя – фазы запуска юнит-тестов никак не клеятся с функциональным тестированием (код -> компиляция -> юнит-тест -> деплой -> функциональный тест), но это более чем терпимо.
Эксперимент удался, на волне хайпа в разработку начали залетать остальные driven’ы – BDD (Behavior-Driven Development), DDD (Domain-driven design) и т.д. Все они методом попугайничества заходили и в область тестирования до того, как появился DDP (Data-driven programming), здесь процесс адаптации в тестирование дал сбой. Парадигма предложила крайне интересную концепцию – программно-аппаратный комплекс может быть спроектирован и построен на основе данных, которые он должен генерировать, если эти данные заранее известны, либо на основе данных, которые генерируют аналогичные системы. Данные как бы являются целью итеративных попыток аппроксимации различных фич приложения, поэтому сразу отделены от кода.
Трансформация DDP –> DDT (Data-driven testing) стала совершенно бессмысленной, потому что тестировщики всегда знали, что оракулом могут служить данные системы-аналога. Статистические механизмы больших данных никак не соотносятся с апогеем функционального тестирования – исследовательским тестированием. Тем не менее, необходимость держать этот дуальный марафон заставила тестировщиков напрячься и придумать хоть что-то связанное с данными, а именно выбрать вторую часть методологии: данные, поступающие в тесты, должны быть отделены от самих тестов, храниться и обрабатываться отдельно, что в теории обеспечивает возможность потокового прогона теста на большом объеме внешних данных без необходимости модифицировать сам тест. Всё это открывает бесконечное пространство комбинаторики в тестировании.

Звучит как нечто очень очевидное, собственно, этим и является, более того, появилась возможность, наконец ввести разумное понятие тестового функционального покрытия (кол-во тестовых данных в строках или файлах / потенциально возможное кол-во уникальных или сгруппированных данных). На момент рождения методологии основные xUnit фреймворки или уже умели работать со внешними данными и порожденными ими зависимостями или рядом появились параллельные фреймворки, которые это предлагали, в мире Java Junit <–> TestNG.
Проблема
Как мы уже выяснили, xUnit фреймворки стали применяться для функционального тестирования и все новшества разработки также успешно воспринимались в тестировании. Функциональное тестирование радостно поглощало результаты unit-тестирования. Рассмотрим два простых примера:
Стандартный юнит-тест, порождающий некоторые данные и проводящий операцию перевода между банковскими счетами (одним из многих способов) и валидирующий состояние объектов:
@Test
public void testMoneyTransfer() {
try {
Account account1 = new Account("счёт А", 100)
Account account2 = new Account("счёт Б", 0)
int amount = account1.getBalance();
account1.transferring(account2, amount);
// перечислим деньги на счёт «Б»
assertEquals(amount, account2.getBalance());
assertEquals(0, account1.getBalance());
} catch (Error e) {
//Capture and append Exceptions/Errors
}
}
Перепишем его, для использования в парадигме DDT:
@DataProvider
public Object[][] testData() {
return new Object[][] {
new Object[] {new Account("счёт А", 100), new Account("счёт Б", 0)}
};
}
@Test(dataProvider = "testData")
public void testMoneyTransfer(Account account1, Account account2) {
try {
int amount = account1.getBalance();
account1.transferring(account2, amount);
assertEquals(amount, account2.getBalance());
assertEquals(0, account1.getBalance());
} catch (Error e) {
//Capture and append Exceptions/Errors
}
}
Выгода очевидна, теперь мы можем «заряжать» в тест большее кол-во данных, выстраивать цепочки трансферов денег с меньшими модификациями кода. Для юнит-теста все выглядит прилично. Теперь превратим эту базу в полноценный функциональный тест, например, добавим запуск браузера или мобильного приложения и действия в интерфейсе для вызова того же метода transferring. Демонстрировать код не буду, для нашей темы он отличается только объемом.
Однако, для функционального теста с большим набором комбинаторных переборов есть одна проблема. Тест получает на вход некую коллекцию данных, в простейшем случае только два счёта. Последовательность объектов коллекции не всегда гарантирована (особенно, если мы небрежно генерируем данные на основе внешнего источника), а тест как не получал ничего, кроме наборов данных, так и не получает. Предположим, что мы достали из корзины первым объектом счёт с нулевым или отрицательным балансом. Если мы пишем юнит-тест – нет ничего тривиальнее, чтобы тесты проходили – просто добавим проверку баланса и переводим средства с того, баланс которого положителен. Это оправдано, потому что мы тестируем не функциональность, а код, нас интересует операция БЕЗ мотивации с определенным исходом. Функциональный тест без мотивации, основанный на данных, – это тест апатичный, данные могут подстраивать под себя бизнес-логику только если мы решаем математическую задачу и занимаемся аппроксимацией к цели. В пространстве объектно-ориентированного программирования мало что так важно, как мотивация, данные её обеспечить не могут! Проиллюстрирую примером реального приложения одного из крупных российских банков.
100 рублей лежат в копилке (счет «Б» в примере выше), на расчетном счету денег нет, но ожидаем получения зарплаты, после чего хотим перевести её в копилку:

Получили смс, что зарплата начислена (ура!), пробуем пополнить копилку (нажимаем кнопку «Пополнить» в копилке), не проверяя дополнительно состояние счета «А», который пока ещё равен 0.

Это невозможное действие, но данные подсказывают приложению выход – нужно перевести из копилки (напоминаю – это счёт «Б») на счёт «А». Приложение, руководствуясь данными, отменило мою мотивацию. В спешке или по рассеянности я могу подтвердить перевод, который не планировался, и потерять проценты.
Я сталкиваюсь с таким поведением систем регулярно и могу примерно представить какие тесты привели к такому плачевному результату. Тестировщик исключил пользователя из уравнения и сосредоточился на данных, возможно даже, что само поведение системы было подогнано под прохождение теста.
Вторым проблемным местом являются «особенные» значения, которые принимают переменные. Разумеется, речь идет о граничных значениях – краеугольном камне тест-дизайна. Очень непросто создать качественный автоматизированный функциональный тест, не выделяя граничные значения из ряда остальных данных.
В-третьих, поддерживать огромный набор данных без сопроводительной информации хотя бы в виде Given When Then хоть сколько-нибудь продолжительное время практически невозможно. Накладные расходы на то, чтобы описать каждую коллекцию чисел, строк или иных данных превышают возможную пользу отделения их от кода, который зачастую выполняет роль псевдодокументации. Впрочем, в этом вопросе могут быть исключения.
Итого
Ни одна концепция из мира разработки не может быть перенесена бездумно в мир тестирования, практически ни одна логическая конструкция из мира unit-тестирования не может бесшовно работать в мире логики функциональной. Мне очень нравится концепция advocacy groups в ИТ процессах, каждая роль в проекте представляет те или иные интересы и имеет разную мотивацию в принятии решений. Мотивация тестировщика – это защита интересов пользователя продукта, мы не можем апатично описывать их только лишь данными и на этой основе строить модель принятия решений и верификации результатов. По этой же причине в ближайшее время AI не сможет вытеснить тестировщиков из функциональных областей (ура!). В большой доле проектов мира ИТ данные влияют друг на друга, данные бывают «особенными», на всё влияет окружение и всем заправляют мотивация и любопытство. Как иначе могли родиться такие чудесные эвристики как пиньята (The Piñata Heuristic), согласно которой мы ломаем пиньяту (приложение) до тех пор, пока не посыпятся конфеты (ошибки), а потом ещё пару ударов, чтобы получить дополнительные возможно оставшиеся конфеты (неочевидные зависимости данных)?