Генератори тестів


Генератор тестів - це програмне забезпечення для конкретного треку, яке автоматично генерує тести для практичної вправи. Він робить це, перетворюючи JSON-тестові випадки вправи на тести мовою треку.

Переваги

Деякі переваги наявності Генератора тестів:

  1. Вправи можна додавати швидше
  2. Автоматизує «нудні» частини додавання вправи
  3. Легко синхронізувати тести з найновішими канонічними даними

Випадки використання

Загалом Генератор тестів запускають, щоб:

  1. Згенерувати тести для нової вправи
  2. Оновити тести наявної вправи

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

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

Оновлення тестів наявної вправи

Коли у вправи вже є Генератор тестів, його можна запустити знову, щоб оновити чи синхронізувати вправу з її найновішими канонічними даними. Радимо робити це періодично, щоб перевіряти, чи немає проблемних тестових випадків, які треба оновити, і нових тестів, які варто додати.

Відправна точка

Є дві можливі відправні точки, коли ми реалізуємо Генератор тестів для вправи:

  1. Вправа нова, тому тестів у неї немає
  2. Вправа вже існує, тому в неї є наявні тести
Caution

Якщо тести вже існують, реалізуйте Генератор тестів так, щоб згенеровані ним тести не ламали наявні рішення.

Проєктування

Загалом файли тестів генерують одним із двох способів:

  • Код: файли тестів (переважно) генеруються кодом
  • Шаблони: файли тестів (переважно) генеруються за допомогою шаблонів

Ми помітили, що підхід на основі коду призводить до доволі складного коду Генератора тестів, тоді як підхід на основі шаблонів простіший.

Радимо такий порядок дій:

  1. Зчитати канонічні дані вправи
  2. Виключити тестові випадки, позначені як include = false у tests.toml вправи
  3. Перетворити канонічні дані вправи на формат, який можна використати в шаблоні
  4. Передати канонічні дані вправи шаблону, специфічному для цієї вправи

Ключова перевага такого підходу в тому, що кожна вправа має власний шаблон, який:

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

Проєктуючи генератор тестів, намагайтеся:

  • Мінімізувати попередню обробку канонічних даних усередині Генератора тестів
  • Зменшити звʼязність між шаблонами

Реалізація

Генератор тестів зазвичай (переважно) написано мовою треку.

Caution

Хоча можна використовувати й інші мови, кожна додаткова мова ускладнить супровід треку та долучення до нього. Тому радимо, де це можливо, використовувати мову треку, бо це полегшує і супровід, і долучення.

Форматування

Якщо в треку є інструменти для форматування коду, варто запускати їх як крок постобробки після рендерингу шаблону.

Канонічні дані

Основні дані, з якими працює Генератор тестів, - це файл canonical-data.json вправи. Цей файл визначено в репозиторії exercism/problem-specifications, який визначає спільні метадані для багатьох вправ Exercism.

Caution

Не всі вправи мають файл canonical-data.json! Якщо його немає, тести доведеться створювати вручну, бо Генератору тестів не буде з чим працювати.

Структура

Канонічні дані визначено в JSON-обʼєкті. Цей обʼєкт містить поле "cases", у якому зберігаються тестові випадки. Ці тестові випадки (зазвичай) відповідають тестам у треку один до одного.

Кожен тестовий випадок має кілька властивостей, найважливіші з яких - опис, властивість, вхідні значення та очікуване значення. Ось (частковий) приклад файлу canonical-data.json вправи leap:

{
  "exercise": "leap",
  "cases": [
    {
      "uuid": "6466b30d-519c-438e-935d-388224ab5223",
      "description": "year not divisible by 4 in common year",
      "property": "leapYear",
      "input": {
        "year": 2015
      },
      "expected": false
    },
    {
      "uuid": "4fe9b84c-8e65-489e-970b-856d60b8b78e",
      "description": "year divisible by 4, not divisible by 100 in leap year",
      "property": "leapYear",
      "input": {
        "year": 1996
      },
      "expected": true
    }
  ]
}

Головне завдання Генератора тестів - перетворювати ці JSON-дані на тести, специфічні для треку. Ось як наведений вище JSON міг би перетворитися на тестовий код мовою Nim:

import unittest
import leap

suite "Leap":
  test "year not divisible by 4 in common year":
    check isLeapYear(2015) == false

  test "year divisible by 4, not divisible by 100 in leap year":
    check isLeapYear(1996) == true

Структуру файлу canonical-data.json добре задокументовано, і для неї також є визначення JSON-схеми.

Вкладеність

Деякі вправи використовують вкладеність у своїх канонічних даних. Це означає, що кожен елемент масиву cases може бути або:

  1. Звичайним тестовим випадком (без дочірніх тестових випадків)
  2. Групою тестових випадків (один або кілька дочірніх тестових випадків)
Note

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

Ось приклад вкладених тестових випадків:

{
  "cases": [
    {
      "uuid": "e9c93a78-c536-4750-a336-94583d23fafa",
      "description": "data is retained",
      "property": "data",
      "input": {
        "treeData": ["4"]
      },
      "expected": {
        "data": "4",
        "left": null,
        "right": null
      }
    },
    {
      "description": "insert data at proper node",
      "cases": [
        {
          "uuid": "7a95c9e8-69f6-476a-b0c4-4170cb3f7c91",
          "description": "smaller number at left node",
          "property": "data",
          "input": {
            "treeData": ["4", "2"]
          },
          "expected": {
            "data": "4",
            "left": {
              "data": "2",
              "left": null,
              "right": null
            },
            "right": null
          }
        }
      ]
    }
  ]
}
Caution

Якщо трек не підтримує групування тестів, потрібно:

  • Обійти (вирівняти) ієрархію cases, щоб залишилися лише найглибші (листові) тестові випадки
  • Поєднати опис тестового випадку з описом його батьків, щоб утворити унікальну назву тесту

Вхідні та очікувані значення

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

Сценарії

Тестові випадки мають необовʼязкове поле scenarios. Генератор тестів може використовувати це поле, щоб особливим чином обробити певні тестові випадки. Найпоширеніший випадок - ігнорувати певні типи тестів, наприклад тести зі сценарієм "unicode", бо мова треку може не підтримувати Unicode.

Повний перелік сценаріїв можна знайти тут.

Читання файлів canonical-data.json

Є кілька способів прочитати файли canonical-data.json:

  1. Завантажити їх безпосередньо з репозиторію problem-specifications (наприклад, https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json).
  2. Додати репозиторій problem-specifications як Git-підмодуль до репозиторію треку.
  3. Прочитати їх із кешу configlet. Розташування залежить від системи користувача, але можна скористатися configlet info -o -v d | head -1 | cut -d " " -f 5, щоб програмно отримати це розташування.

Специфічні для треку тестові випадки

Якщо трек хоче додати додаткові тестові випадки, специфічні для нього (яких немає в канонічних даних), один із варіантів - створити файл additional-test-cases.json, який Генератор тестів потім може обʼєднати з файлом canonical-data.json, перш ніж передати його шаблону для рендерингу.

Шаблони

Рушій шаблонів, який використовуватиметься, імовірно, буде специфічним для треку. В ідеалі шаблони мають бути якомога простішими, тож не варто перейматися дублюванням коду тощо.

Самі шаблони отримують свої дані від Генератора тестів, які вони перебирають, щоб відрендерити себе.

Note

Щоб шаблони залишалися простими, може бути корисно трохи попередньо обробити дані на боці Генератора тестів або визначити якісь «фільтри» чи інший механізм розширення, який підтримують шаблони.

Використання configlet

configlet - це основний інструмент для супроводу треку, і його можна використовувати, щоб:

  • Створити файли вправи для нової вправи: запустіть bin/configlet create --practice-exercise <slug>
  • Синхронізувати файл tests.toml наявної вправи: запустіть bin/configlet sync --tests --update --exercise <slug>
  • Завантажити канонічні дані вправи на диск (це побічний ефект будь-якої з наведених вище команд)

Завдяки цьому configlet - чудовий інструмент, який можна поєднувати з Генератором тестів для справді потужних робочих процесів.

Інтерфейс командного рядка

Варто зробити використання Генератора тестів і простим, і потужним. Для цього радимо створити один або кілька файлів скриптів.

Note

Можна вибрати будь-який формат файлу скриптів, який найкраще пасує треку. Скрипти оболонки та скрипти PowerShell - поширені варіанти, і обидва можуть добре працювати.

Ось приклад скрипта оболонки, який поєднує configlet і Генератор тестів, щоб швидко створити каркас нової вправи:

bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>

Створення з нуля

Перш ніж починати створювати Генератор тестів, радимо подивитися на кілька наявних Генераторів тестів, щоб відчути, як інші треки їх реалізували:

Якщо виникнуть запитання, форум - найкраще місце, щоб їх поставити. Обговорення на форумі про Генератор тестів для Rust і для JavaScript теж можуть бути корисними.

Мінімально життєздатний продукт

Радимо будувати Генератор тестів поступово, починаючи з мінімально життєздатного продукту. Найпростіша версія читала б canonical-data.json вправи й просто передавала б ці дані шаблону.

Почніть з однієї вправи, бажано простої, як-от leap. І лише коли це працюватиме, поступово додавайте більше вправ.

І намагайтеся тримати Генератор тестів якомога простішим.

Note

В ідеалі контрибʼютор міг би просто вставити чи змінити наявний шаблон, навіть не розуміючи, як Генератор тестів працює всередині.

Використання та долучення

Те, як використовувати Генератор тестів чи долучатися до нього, залежить від треку. Шукайте вказівки в README.md, CONTRIBUTING.md треку або в каталозі з кодом Генератора тестів.