Чи можна автоматизувати не тестування, а створення тестів?

Як правило, це складніша задача, але і її можна вирішити в окремих (і при цьому доволі поширених) випадках. Тест-дизайн на основі даних, коли сценарії відносно незмінні, але міняються вхідні дані, перший і очевидний кандидат при спробі автоматизувати цей процес. Тести, які базуються на поведінці, станах і переходах, — більш суперечлива область, тому у цій статті ми не будемо про неї говорити.

Розглянемо відносно стандартні підходи до автоматичної генерації даних, інструменти, які реалізують деякі з них, сфери їх застосування (тут, на мою думку, йдеться не про переваги і недоліки, а, скоріше, про вибір оптимального інструменту для конкретної задачі).

Випадкова генерація

Ідея: дані належать до тих чи інших множин і діапазонів, кожен тест це комбінація значень, які обираються випадково з відповідної множини/діапазону (класу еквівалентності).

Властивості:

  1. Дуже легко реалізувати, є інструменти, які дозволяють автоматично генерувати синтаксично коректні юніт-тести (у рамках синтаксису тієї чи іншої МП та/або бібліотеки).
  2. Можна швидко згенерувати багато тестів. 
  3. Виявлення помилок настільки ж випадкове, як і вхідні дані.

Сфери застосування:

  1. Фазинг
  2. Невеликі програми або частини програм, де кількість елементів всередині класів еквівалентності на надто велика.

Генерація на підставі алгоритмів пошуку

Ідея: підходити до генерації даних як до класичної задачі оптимізації, тобто задати цільову функцію і мінімально потрібний поріг досягнення результату, обмеження (якщо це потрібно), класи еквівалентності для вхідних даних (останнє, звичайно, є обов’язковою умовою для всіх підходів до вирішення цієї задачі).

Властивості:

  1. Можна «загрузнути» у локальному оптимумі, якщо мінімально необхідний поріг занадто низький (тим не менше, задача буде вирішена).
  2. Як наслідок п.1, часто може існувати кілька розв’язків задачі, і немає впевненості, що той, який було згенеровано, найкращий.
  3. Важливо розуміти, що, як і під час випадкової генерації, такі тести не беруть до уваги бізнес-логіку, техніки тест-дизайну тощо, тому якість тестування і, відповідно, знайдені баги також відносно випадкові.

Сфери застосування:

  1. Дуже широкий діапазон задач, є інструменти, які використовують цей підхід як для генерації юніт-тестів, так і для випадкової генерації.
  2. Як правило, у якості цільового показника використовують відсоток покриття і задають необхідне мінімальне значення.

Генетичні алгоритми

Ідея: згенерувати дуже багато наборів даних (популяції), міняти компоненти між наборами (кросовер, в результаті ми отримуємо набір, який мутував), для кожного набору даних (у тому числі вихідних) вимірювати значення цільової функції і порівнювати з мінімально необхідним порогом. Загалом це схоже на погляд з точки зору теорії оптимізації, але є можливість вийти з локального оптимуму в результаті кросовера, тобто, ймовірно, результати будуть кращими. Але й працювати такі алгорими будуть повільніше, ніж ті, які ми розглянули вище. 

Властивості:

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

Сфери застосування:

  1. Академічний інтерес. Через обмеження у продуктивності не знайшла реальних випадків застосування генетичних алгоритмів. 
  2. Кілька цільових показників, котрі потрібно враховувати одночасно.

Генерація тестових послідовностей

Ідея: класичні комбінаторні техніки тест-дизайну — взяти параметри, обрати значення, задати обмеження (правила комбінування) й отримати усі можливі комбінації, які відповідають обмеженням. Pairwise є, зокрема, прикладом цього підходу, як і причина/наслідок (хоча це й більш загальний метод).

Властивості:

  1. На вхід таким алгоритмам надається формальна модель, тобто параметри і значення, котрі вони приймають, умови, які накладаються на комбінації таких значень, параметри комбінацій (наприклад, кожен з кожним, лише пари, лише трійки і т.і.).  Таким чином, результат проектування повністю передбачуваний і володіє винятково тим набором властивостей, котрі закладені у моделі.
  2. На додаток до правил генерації і заданих залежностей між значеннями параметрів можна встановлювати вагу значень. Таким чином, можна регулювати частоту, з якою значення зустрічаються у тестах: там, де у комбінаціях байдуже, яке значення обрати, вага задає ймовірність такого вибору.
  3. Крім ваги значень, можна задавати і пріоритизацію, тобто порядок, за яким тести з’являтимуться у наборі. Погана новина у тому, що ця функція є не в усіх інструментах.

Сфери застосування:

  1. Тести на будь-якому рівні, які враховують бізнес-логіку системи, оскільки формальна модель таких алгоритмів на вході задається її автором. 
  2. Важливо розуміти, що інструменти, котрі працюють з таким підходом, будуть генерувати лише дані (комбінації значень), а не самі тести, про який би рівень не йшлося (ціна за враховану бізнес-логіку системи. Це означає, що як юніт-тести, так і автотести на системному рівні доведеться писати самостійно. Зате дані для них можна буде зчитувати з файлів, отриманих за допомогою інструментів.

Інструменти

Нижче мова піде про інструменти, що реалізують деякі з описаних вище підходів. Обговоримо, як користуватися, який результат отримаємо, який підхід використовується. Можна порівняти і обрати те, що підходить для конкретної задачі, або, як мінімум, визначити, що шукати далі.

Генерація юніт-тестів на Java з Randoop (випадкова генерація)

Як це працює:

  1. Створити файл myclasses.txt зі списком імен класів, які потрібно тестувати.
  2. Викликати Randoop: java -classpath $(RANDOOP_JAR) randoop.main.Main gentests --classlist=myclasses.txt --time-limit=60
  3. Скомпілювати і запустити тести, котрі згенерував Randoop. Отримаємо два набори: ErrorTest і RegressionTest. Тести з першого набору не пройдуть (ймовірно, це баг, необхідне додаткове дослідження). Тести з другого набору пройдуть успішно.

Що приблизно отримаємо:

Генерація юніт-тестів на Java з EvoSuite (алгоритми пошуку)

Як це працює:

  1. Визначитися з класом, який буде тестуватися, зі шляхом до цього класу і його залежностями.
  2. Визначитися з цільовою функцією.
  3. Викликати EvoSuite, наприклад, так (задано цільовий параметр): $EVOSUITE -class <ClassName> -projectCP <ClassPath> -criterion branch

Що приблизно отримаємо:

 

Генерація даних з PICT (комбінаторне тестування)

Як це працює:

  1. Задати модель та її обмеження, наприклад, так^

 2. Запустити PICT, передавши модель на вхід, переспрямувати вивід у файл у разі потреби: pict.exe model.txt > results.csv

Що приблизно отримаємо:

Для моделі, показаної вище, скажімо, отримаємо файл (або вивід у консоль) з такими даними:

Як з цим працювати далі на розсуд автора моделі чи інших причетних до процесу.

 Корисні посилання

Висновки

Генерація тестових даних річ, безумовно, корисна, але не достатньо прив’язана до бізнес-логіки системи, за винятком тестових послідовностей. Як тестувальник, я частіше застосовую саме останній клас інструментів, зокрема PICT.

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

На юніт-рівні, за який найчастіше відповідають програмісти, значно частіше використовуються підходи та інструменти, що базуються на алгоритмах пошуку. Це дозволяє досягати поставлених цілей за рівнем покриття коду, хоча у більшості випадків це не забезпечує належного рівня якості. EvoSuite – досить популярний інструмент для вирішення саме цієї задачі. 

Оригінал статті