Блог

Як працює EasyTest, тестовий фреймворк для JavaScript/TypeScript

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

Передумови створення

Чому я вирішив створити свій фреймворк? Я багато пишу кодів на Javascript, тож його треба якось тестувати. Звичайно, ви скажете, що вже є JEST, VITEST та інші. Але мені захотілось створити власний! По-перше — це чудовий засіб підвищити свої навички в JavaScript, по-друге — розуміти, як такі фреймворки працюють «під капотом» може дуже сильно допомогти в плануванні тестування власного коду. Ну і в загалі — чи зможу?

Планування функціоналу

Перше, з чого необхідно починати будь-який проєкт — це планування його функціоналу. Що я хотів бачити у своєму фреймворку:

Типи тестів — можливість писати:

  1. Юніт-тести
  2. Інтеграційні тести

Функціонал

  1. Легкий початок роботи з фреймворком (config free).

  2. Тестування JavaScript та TypeScript коду без зайвого клопоту.

  3. Тестування асинхронного коду.

  4. Тестування HTML об’єктів (Document, HTMLElement, …).

  5. Mocking (функції та об’єкти).

  6. Багато очікувань (expect) в одному тесті — тест вважається виконаним, якщо всі очікування завершились без помилок.

  7. Велика кількість вбудованих matchers (функцій перевірки).

  8. Можливість розширення переліку доступних matchers прямо в тестах.

  9. Підтримка стандартних функцій describe, it, test, and expect.

  10. Підтримка функцій Setup та Teardown (beforeEach, beforeAll, afterEach, afterAll).

  11. Можливість формування звіту щодо покриття коду (в тому числі можливість взаємодії з CODECOV).

  12. Можливість писати тести як на JS, так і на TS та комбінувати їх в одному проєкті

Архітектура фреймворку

Фреймворк містити декілька структурних компонентів:

  • Створювач черги виконання тестів.

  • Виконувач тестів.

  • Модуль Assertion.

  • Інструменти Mocking.

  • Профайлер для генерування звіту про покриття коду тестами.

  • Репортер для формування звіту про покриття коду тестами в форматі LCOV.

Створювач черги виконання тестів

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

  • beforeAll — виконати код перед всіма тестами
  • beforeEach — виконати код перед кожним тестом
  • afterEach — виконати код після кожного тесту
  • afterAll — виконати код після всіх тестів

beforeAll

beforeAll буде виконано на початку файлу, так і на початку набору тестів, залежно від того в якому місці він об’явлений.


beforeAll (() => {
                // Буде виконано на початку файла
})
describe (``, () => {
                beforeAll (() => {
                            // Буде виконано на початку набора тестів
                })

              it (…)
})


beforeEach

beforeEach буде виконано перед усіма тестами у файлі, якщо він об’явлений на початку файлу, або перед кожним тестом у наборі, якщо він об’явлений у середині функції describe.


beforeEach (() => {
         // Буде виконано перед кожним тестом в файлі
})
describe (``, () => { beforeEach (() => {
        beforeEach (() => {
                    // Буде виконано перед кожним тестом
                    // в поточному наборі тестів })
         })

         it (…)
})


afterEach

afterEach буде виконано після всіх тестів у файлі, якщо він об’явлений на початку файлу, або після кожного тесту в наборі, якщо він об’явлений у середині функції describe.

afterAll буде виконано або після тестів у наборі, або наприкінці файлу.

Виклики цих функцій можна комбінувати в одному файлі як глобально, так і локально для конкретного describe.

Створювач черги гарантує, що тести та функції встановлення та демонтажу будуть виконані саме в тому порядку, як вони зазначені.

Виконувач тестів

Після того, як чергу виконання створено, вона передається на виконання виконувачу тестів. Виконувач тестів виконує тести, враховуючи функції установки та демонтажу. Кожен тест — це набір очікувань (expects), які треба виконати. Невиконання будь-якого очікування (expect) приводить до припинення подальшої обробки відповідного тесту (it, test).

Модуль Assertion

Виконувач тестів використовує виклики модуля Assertion для обчислення очікувань. Запуск очікування використовується за допомогою функції expect з передачею в цю функцію значення, яке необхідно перевірити. Функція expect повертає об’єкт Expect, який містить набір matchers — функцій перевірки.

Функції перевірки можуть приймати контрольне значення, з яким проводиться зіставлення та користувацьке повідомлення на випадок, якщо перевірку не пройдено. На зараз об’єкт Expect містить понад 100 вбудованих функцій перевірки. Це і просте зіставлення, і суворе, і перевірка структур об’єктів і перевірка масивів (наприклад на унікальність). До речі, якщо вам недостатньо цих функцій, ви з легкістю можете додати власні. Про це буде далі.

Якщо перевірку не пройдено, функція перевірки формує Throw exception з відповідним повідомленням та значеннями, які зіставлялися, та припиняє виконання поточного тесту, і цей тест тепер вважається проваленим.

Інструменти мокінгу (mocking)

Функції-імітації (mocking functions) значно спрощують тестування пов’язаного коду, надаючи можливість стирати справжню імплементацію функції, записувати виклики функції (і параметри, які були їй передані), записувати екземпляри, які повертає функція-конструктор, викликана з допомогою оператора new, і вказувати значення, які має повернути функція під час тестування.

Наразі фреймворк підтримує створення mock функції за допомогою фабричного методу mocker (). За допомогою цих функцій ви можете тестувати виклики та передавати параметри.


describe (`Test mocking`, () => {
         const mock = mocker ()
         mock ()
         expect (mock).toHaveBeenCalled ()
})


Профайлер-генератор звіту покриття коду тестами

Якщо ввімкнуто функцію генерації звіту про покриття коду тестами за допомогою параметра coverage (cli аргумент --coverage), фреймворк після виконання тестів, формує звіт щодо кількісного покриття коду тестами. Вбудований репортер створить файл звіту в форматі LCOV. Який можна, наприклад, завантажити в CODECOV.

Профайлер у своїй роботі використовує модуль node:inspector. Модуль node:inspector надає API для взаємодії з інспектором V8. Що своєю чергою дає можливість отримати звіт щодо використання тестуємого коду.

Після того, як профайлер сформував звіт покриття, цей звіт передається в модуль генерації LCOV файлу. Згенерований файл може бути використаний із будь-яким інструментом аналізу покриття коду, який вміє працювати з форматом LCOV, наприклад, CODECOV.

Встановлення

Щоб встановити фреймворк, потрібно виконати команду


npm i @olton/easytest


Створимо перший простий тест (наприклад в каталозі __tests__/simple.test.js)


import { describe, it, expect } from '@olton/easytest';

describe ('My Tests', () => {
       it ('should 1 === 1', () => {
             expect (1).toBe (1); });
        });
});


Налаштування

EasyTest розроблений як config-free фреймворк, тобто для своєї роботи він не потребує обов’язкового створення конфігураційного файлу. За замовченням використовуються такі параметри:


{
include: [»**/*.spec.{t,j}s», «**/*.spec.{t,j}sx»,
»**/*.test.{t,j}s»,
»**/*.test.{t,j}sx»
],
exclude: [«node_modules/**»],
coverage: false,
verbose: false,
report: {
       type: «lcov»,
       dir: «coverage», }
}


Щоб змінити параметр за замовченням, ви можете створити файл конфігурації з ім’ям easytest.json (або будь-яким іншим ім’ям, але тоді необхідно буде про це сказати фреймворку за допомогою cli аргументу –config)

Запуск

Щоб запустити easytest, необхідно виконати команду:


npx easytest


або додати в package.json


{
     «scripts»: {
             «test»: «easytest» }
     }


і потім використовувати команду:


npm test


Аргументи командного рядка:

  • –config=config_file_name.json — шлях до користувацького конфігураційного файлу

  • –verbose — багатослівність або детальний лог виконання (наразі вивід відбувається в консоль)

  • –coverage — сформувати звіт покриття коду тестами

  • –test — виконати лише тести, ім’я яких збігатися із вказаним шаблоном

  • –include=’…’ — де шукати тести

  • –exclude=’…’ — які файли або теки не враховувати при пошуку тестів

Підтримка TypeScript

Щоб додати підтримку тестування TypeScript коду необхідно встановити модуль tsx.


npm i -D tsx cross-env


cross-env додасть можливість міжплатформового встановлення змінної NODE_OPTIONS.

Щоб використати можливості tsx необхідно додати змінну оточення NODE_OPTIONS зі значенням «–import tsx». Змінить команду запуску easytest:


{
       «scripts»: {
                 «test»: «cross-env NODE_OPTIONS=\"--import tsx\» easytest»
       }
}


Це все, що потрібно зробити для тестування коду, написаного на TypeScript та написання тестів на TypeScript.

Вивантаження звіту на зовнішній ресурс

Нижче наведено приклад GitHub автоматизації для автоматичного тестування коду при push та вивантаження звіту на CODECOV


name: Run tests and upload coverage
on:
push

jobs:
   test:
       name: Run tests and collect coverage
       runs-on: ubuntu-latest
       strategy:
              matrix:
                        node-version: ['22.x']
       steps:
              — name: Checkout
                  uses: actions/checkout@v4
                  with:
                         fetch-depth: 0
              — name: Set up Node
                  uses: actions/setup-node@v4
                  with:
                         node-version: ${{ matrix.node-version }}
              — name: Install dependencies
                  run: npm install
              —name: Run tests
                 run: easytest --coverage
              —name: Upload results to Codecov
                 uses: codecov/codecov-action@v4
                with:
                         token: ${{ secrets.CODECOV_TOKEN }}


Результат на CODECOV

Розширення функціоналу

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


import {Expect, ExpectError} from «@olton/easytest»;

class MyExpect extends Expect {
    toBeEven () {
             let received = this.received
             let result = received % 2 === 0
             if (!result) {
                      throw new ExpectError (`Expected ${received} to be even`, ‘toBeEven’, received, ‘Even’)
              }
     }
}

const expect = (received) => new MyExpect (received)

   test (`Custom expect`, () => {
       expect (2).toBeEven ()
   })


Приклади тестів

Ви можете використовувати EasyTest для перевірки компонентів інтерфейсу користувача. У цьому прикладі я тестую акордеонний компонент Metro UI.


import fs from «fs»;
import {beforeAll, describe, it, expect} from «@olton/easytest»;

beforeAll (() => {
      window.METRO_DISABLE_BANNER = true;
      window.METRO_DISABLE_LIB_INFO = true;
      document.body.innerHTML = `
      <div id="accordion">
              <div class="frame">
                       <div class="heading">Heading</div>
                       <div class="content">Content</div>
              </div>
      </div>
'
      window.eval (fs.readFileSync ('./lib/metro.js', 'utf8')) 
})

describe (`Accordion tests`, () => {
        it (`Create accordion`, async () => {
              const accordion = window.Metro.makePlugin (»#accordion»,
'accordion')[0]
              expect (accordion).hasClass ('accordion')
         })
})


Ще більше тестів за посиланням.

Підсумок

Проєкт вийшов дуже цікавим, дав змогу отримати нові знання та поглибити наявні навички в JavaScript.

Проєкт зараз перебуває в активній розробці.