Чи можна автоматизувати не тестування, а створення тестів?
Як правило, це складніша задача, але і її можна вирішити в окремих (і при цьому доволі поширених) випадках. Тест-дизайн на основі даних, коли сценарії відносно незмінні, але міняються вхідні дані, — перший і очевидний кандидат при спробі автоматизувати цей процес. Тести, які базуються на поведінці, станах і переходах, — більш суперечлива область, тому у цій статті ми не будемо про неї говорити.
Розглянемо відносно стандартні підходи до автоматичної генерації даних, інструменти, які реалізують деякі з них, сфери їх застосування (тут, на мою думку, йдеться не про переваги і недоліки, а, скоріше, про вибір оптимального інструменту для конкретної задачі).
Випадкова генерація
Ідея: дані належать до тих чи інших множин і діапазонів, кожен тест — це комбінація значень, які обираються випадково з відповідної множини/діапазону (класу еквівалентності).
Властивості:
- Дуже легко реалізувати, є інструменти, які дозволяють автоматично генерувати синтаксично коректні юніт-тести (у рамках синтаксису тієї чи іншої МП та/або бібліотеки).
- Можна швидко згенерувати багато тестів.
- Виявлення помилок настільки ж випадкове, як і вхідні дані.
Сфери застосування:
- Фазинг
- Невеликі програми або частини програм, де кількість елементів всередині класів еквівалентності на надто велика.
Генерація на підставі алгоритмів пошуку
Ідея: підходити до генерації даних як до класичної задачі оптимізації, тобто задати цільову функцію і мінімально потрібний поріг досягнення результату, обмеження (якщо це потрібно), класи еквівалентності для вхідних даних (останнє, звичайно, є обов’язковою умовою для всіх підходів до вирішення цієї задачі).
Властивості:
- Можна «загрузнути» у локальному оптимумі, якщо мінімально необхідний поріг занадто низький (тим не менше, задача буде вирішена).
- Як наслідок п.1, часто може існувати кілька розв’язків задачі, і немає впевненості, що той, який було згенеровано, найкращий.
- Важливо розуміти, що, як і під час випадкової генерації, такі тести не беруть до уваги бізнес-логіку, техніки тест-дизайну тощо, тому якість тестування і, відповідно, знайдені баги також відносно випадкові.
Сфери застосування:
- Дуже широкий діапазон задач, є інструменти, які використовують цей підхід як для генерації юніт-тестів, так і для випадкової генерації.
- Як правило, у якості цільового показника використовують відсоток покриття і задають необхідне мінімальне значення.
Генетичні алгоритми
Ідея: згенерувати дуже багато наборів даних (популяції), міняти компоненти між наборами (кросовер, в результаті ми отримуємо набір, який мутував), для кожного набору даних (у тому числі вихідних) вимірювати значення цільової функції і порівнювати з мінімально необхідним порогом. Загалом це схоже на погляд з точки зору теорії оптимізації, але є можливість вийти з локального оптимуму в результаті кросовера, тобто, ймовірно, результати будуть кращими. Але й працювати такі алгорими будуть повільніше, ніж ті, які ми розглянули вище.
Властивості:
- Можна отримати кілька рішень, які задовольняють цільову функці
- Можна мати кілька цільових функцій, оскільки спочатку створюються набори і лише потім вимірюється цільовий показник. Цей підхід дозволяє взяти стільки цільових функцій, скільки потрібно, і розглядати їх усі одночасно
Сфери застосування:
- Академічний інтерес. Через обмеження у продуктивності не знайшла реальних випадків застосування генетичних алгоритмів.
- Кілька цільових показників, котрі потрібно враховувати одночасно.
Генерація тестових послідовностей
Ідея: класичні комбінаторні техніки тест-дизайну — взяти параметри, обрати значення, задати обмеження (правила комбінування) й отримати усі можливі комбінації, які відповідають обмеженням. Pairwise є, зокрема, прикладом цього підходу, як і причина/наслідок (хоча це й більш загальний метод).
Властивості:
- На вхід таким алгоритмам надається формальна модель, тобто параметри і значення, котрі вони приймають, умови, які накладаються на комбінації таких значень, параметри комбінацій (наприклад, кожен з кожним, лише пари, лише трійки і т.і.). Таким чином, результат проектування повністю передбачуваний і володіє винятково тим набором властивостей, котрі закладені у моделі.
- На додаток до правил генерації і заданих залежностей між значеннями параметрів можна встановлювати вагу значень. Таким чином, можна регулювати частоту, з якою значення зустрічаються у тестах: там, де у комбінаціях байдуже, яке значення обрати, вага задає ймовірність такого вибору.
- Крім ваги значень, можна задавати і пріоритизацію, тобто порядок, за яким тести з’являтимуться у наборі. Погана новина у тому, що ця функція є не в усіх інструментах.
Сфери застосування:
- Тести на будь-якому рівні, які враховують бізнес-логіку системи, оскільки формальна модель таких алгоритмів на вході задається її автором.
- Важливо розуміти, що інструменти, котрі працюють з таким підходом, будуть генерувати лише дані (комбінації значень), а не самі тести, про який би рівень не йшлося (ціна за враховану бізнес-логіку системи. Це означає, що як юніт-тести, так і автотести на системному рівні доведеться писати самостійно. Зате дані для них можна буде зчитувати з файлів, отриманих за допомогою інструментів.
Інструменти
Нижче мова піде про інструменти, що реалізують деякі з описаних вище підходів. Обговоримо, як користуватися, який результат отримаємо, який підхід використовується. Можна порівняти і обрати те, що підходить для конкретної задачі, або, як мінімум, визначити, що шукати далі.
Генерація юніт-тестів на Java з Randoop (випадкова генерація)
Як це працює:
- Створити файл myclasses.txt зі списком імен класів, які потрібно тестувати.
- Викликати Randoop: java -classpath $(RANDOOP_JAR) randoop.main.Main gentests --classlist=myclasses.txt --time-limit=60
- Скомпілювати і запустити тести, котрі згенерував Randoop. Отримаємо два набори: ErrorTest і RegressionTest. Тести з першого набору не пройдуть (ймовірно, це баг, необхідне додаткове дослідження). Тести з другого набору пройдуть успішно.
Що приблизно отримаємо:
Генерація юніт-тестів на Java з EvoSuite (алгоритми пошуку)
Як це працює:
- Визначитися з класом, який буде тестуватися, зі шляхом до цього класу і його залежностями.
- Визначитися з цільовою функцією.
- Викликати EvoSuite, наприклад, так (задано цільовий параметр): $EVOSUITE -class <ClassName> -projectCP <ClassPath> -criterion branch
Що приблизно отримаємо:
Генерація даних з PICT (комбінаторне тестування)
Як це працює:
- Задати модель та її обмеження, наприклад, так^
2. Запустити PICT, передавши модель на вхід, переспрямувати вивід у файл у разі потреби: pict.exe model.txt > results.csv
Що приблизно отримаємо:
Для моделі, показаної вище, скажімо, отримаємо файл (або вивід у консоль) з такими даними:
Як з цим працювати далі — на розсуд автора моделі чи інших причетних до процесу.
Корисні посилання
Висновки
Генерація тестових даних — річ, безумовно, корисна, але не достатньо прив’язана до бізнес-логіки системи, за винятком тестових послідовностей. Як тестувальник, я частіше застосовую саме останній клас інструментів, зокрема PICT.
Основна складність тут не в інструментах, а в моделюванні, особливо для систем зі складною логікою (що зустрічається не так уже й часто). Випадкова генерація у контексті тестування найчастіше була корисною у фазінгу, але й тут доводилося винаходити кілька велосипедів, писати свою логіку генерації випадкових даних замість використання стандартних інструментів.
На юніт-рівні, за який найчастіше відповідають програмісти, значно частіше використовуються підходи та інструменти, що базуються на алгоритмах пошуку. Це дозволяє досягати поставлених цілей за рівнем покриття коду, хоча у більшості випадків це не забезпечує належного рівня якості. EvoSuite – досить популярний інструмент для вирішення саме цієї задачі.