Репозиторій EasyTest: https://github.com/olton/easytest

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

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

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

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

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

Типи тестів:

  • Юніт-тести
  • Інтеграційні тести

Можливості:

  • Легкий початок роботи з фреймворком (config free)
  • Тестування JavaScript та TypeScript коду без зайвого клопоту
  • Тестування асинхронного коду
  • Тестування HTML об’єктів (Document, HTMLElement, …)
  • Mocking (функції та об’єкти)
  • Багато очікувань (expect) в одному тесті - тест вважається виконаним, якщо всі очікування завершились без помилок.
  • Велика кількість вбудованих matchers (функцій перевірки)
  • Можливість розширення переліку доступних matchers прямо в тестах
  • Підтримка стандартних функцій describe, it, test, and expect
  • Підтримка функцій Setup та Teardown (beforeEach, beforeAll, afterEach, afterAll)
  • Можливість формування звіту щодо покриття коду (в тому числі можливість взаємодії з CODECOV)
  • Можливість писати тести як на JS, так і на TS та комбінувати їх в одному проєкті

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

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

  • Створювач черги виконання тестів
  • Виконувач тестів
  • Модуль Assertion
  • Інструменти Mocking
  • Профайлер для генерування звіту про покриття коду тестами
  • Репортер для формування звіту про покриття коду тестами в форматі LCOV

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

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

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

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

beforeAll(() => {
    // Буде виконано на початку файла
})


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


    it(...)
})

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

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


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


    it(...)
})

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 -D @olton/easytest

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

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

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

До речі можна не імпортувати describe, it, test та expect, бо вони доступні в глобальному контексті.

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

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

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()
})

Тестування HTML UI

Ви можете використовувати 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')
   })
})

Ще більше тестів ви знайдете за посиланням: https://github.com/olton/easytest/tree/master/__tests__

Висновок

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

Посилання на GitHub - https://github.com/olton/easytest

Проєкт зараз находиться в активній розробці.