Покращуємо якість коду. Як я створив тестовий фреймворк для JavaScript і TypeScript

Привіт! Мене звати Сергій Піменов. Я веброзробник із Києва і дуже відданий своїй роботі. Маю більш ніж 25-річний досвід роботи програмістом. Понад 10 з них — у професійній веброзробці.

Зараз працюю на позиції Front-End Lead в Korzh.com. Займаюсь створенням вебкомпонентів за допомогою HTML, JavaScript і CSS, а також сайтів з використанням Astro та Metro UI. Я є великим прихильником проєктів з відкритим кодом, тож майже все мої проєкти є open source та доступні на GitHub.

У цій статті розповім про власний тестовий фреймворк для 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=’...’ — шлях до користувацького конфігураційного файлу.
  • verbose — багатослівність або детальний лог виконання (наразі вивід відбувається в консоль).
  • coverage — сформувати звіт покриття коду тестами.
  • test=’...’ — виконати лише тести, ім’я яких збігатися із вказаним шаблоном.
  • include=’...’ — де шукати тести.
  • exclude=’...’ — які файли або теки не враховувати при пошуку тестів.

Підтримка TypeScript

Однією із умов була проста можливість тестувати TypeScript-код та писати тести на TypeScript. В EasyTest, щоб запровадити підтримку тестування 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()
})

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

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

import {beforeAll, beforeEach, describe, it, expect, DOM} from "@olton/easytest";
beforeAll(() => {
    DOM.flash()
    DOM.js.fromFile('./lib/metro.js')
})
beforeEach(() => {
    DOM.html.fromString(`
        <div id="accordion">
            <div class="frame">
                <div class="heading">Heading</div>
                <div class="content">Content</div>
            </div>
        </div>
    `)
})
describe(`Accordion tests`, () => {
    it(`Create accordion`, async () => {
        const accordion = window.Metro.makePlugin("#accordion", 'accordion')[0]
        expect(accordion).hasClass('accordion')
    })
})

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

Висновок

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

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Класна робота! Дякую Сергій

Підписатись на коментарі