Szybki kod języka programowania z osobą i laptopem.

Chai.js – assert, expect i should w testach JavaScript

11 min. czytania

Trzy style asercji w bibliotece Chai.js — assert, expect i should — różnią się składnią, ergonomią i szczegółami technicznymi, ale każdy z nich pozwala precyzyjnie wyrazić oczekiwania wobec kodu w testach JavaScript. Chai to jedna z najpopularniejszych bibliotek asercji używana z frameworkami takimi jak Mocha, oferująca styl TDD (assert) oraz BDD (expect, should).

Czym jest Chai i po co ci trzy style asercji?

Chai to biblioteka asercji dla JavaScript, czyli narzędzie do sprawdzania w testach, czy kod zachowuje się zgodnie z oczekiwaniami. Działa w Node.js i w przeglądarce i najczęściej łączy się ją z runnerami testów, takimi jak Mocha.

Zamiast ręcznie pisać instrukcje warunkowe typu:

if (result !== 42) {
throw new Error('result powinien być równy 42');
}

w Chai piszesz:

expect(result).to.equal(42);

lub:

assert.equal(result, 42);

Chai udostępnia trzy style asercji:

  • Assert – klasyczny styl TDD, oparty na funkcjach (np. assert.equal(...));
  • Expect – styl BDD, czytelne, łańcuchowe asercje (np. expect(foo).to.be.a('string'));
  • Should – styl BDD, który dodaje właściwość should do obiektów (np. foo.should.be.a('string')).

Wszystkie trzy style robią to samo — zgłaszają błąd testu, jeśli warunek nie jest spełniony — różnią się jednak składnią i zachowaniem technicznym.

Instalacja Chai (z Mocha) — szybki start

Najpopularniejszy zestaw to Mocha + Chai. Instalacja za pomocą npm (jako dev-dependencje):

npm install mocha chai --save-dev

Przykładowa struktura projektu:

projekt/
src/
sum.js
test/
sum.spec.js
package.json

W package.json dodaj skrypt testowy Mocha:

{
"scripts": {
"test": "mocha"
}
}

Import Chai w wybranym stylu:

// assert
const assert = require('chai').assert;

// expect
const expect = require('chai').expect;

// should
const should = require('chai').should();

Styl assert — klasyczny TDD

Charakterystyka

Styl assert jest najbardziej zbliżony do wbudowanej w Node.js biblioteki assert. Używasz funkcji assert.*, przekazując wartości i opcjonalny komunikat błędu.

var assert = require('chai').assert;

assert.equal(actual, expected, 'komunikat w razie błędu'); // porównanie ==
assert.strictEqual(actual, expected); // porównanie ===
assert.deepEqual(obj1, obj2); // głębokie porównanie obiektów/tablic
assert.isTrue(value);
assert.isFalse(value);
assert.isNull(value);

Przykład w kontekście Mocha:

const assert = require('chai').assert;
const sum = require('../src/sum');

describe('sum() – styl assert', () => {
it('powinno zwrócić poprawną sumę', () => {
const result = sum(2, 3);
assert.equal(result, 5, 'wynik dodawania 2 + 3 powinien być równy 5');
});
});

API assert w Chai jest szerokie — obejmuje m.in. assert.ok, assert.fail, assert.include, assert.isEmpty i wiele innych metod.

Zalety stylu assert

  • brak modyfikacji prototypów – styl assert nie zmienia Object.prototype, co jest bezpieczne w środowiskach z kodem współdzielonym;
  • prostota funkcji – każda asercja to zwykłe wywołanie funkcji, zrozumiałe dla osób przyzwyczajonych do TDD;
  • własne komunikaty – w większości metod możesz dodać własny komunikat, co ułatwia diagnozowanie błędów.

Wady stylu assert

Mniej „naturalnego” języka — asercje są krótkie, ale trudniej z nich ułożyć czytelną specyfikację w stylu BDD.

Brak łańcuchowego API — dla złożonych warunków zapis bywa rozbity na wiele wywołań zamiast jednego, czytelnego „zdania”.

Styl expect — BDD, najbardziej czytelny

Charakterystyka

Styl expect to jedna z dwóch składni BDD w Chai i często rekomendowany wybór w nowych projektach. Naturalny, łańcuchowy zapis poprawia czytelność testów.

var expect = require('chai').expect;

expect(actual).to.equal(expected);
expect(actual).to.be.a('string');
expect(arr).to.have.lengthOf(3);

Łańcuchowe „słowa” (to, be, have, with, and) są „puste” — poprawiają czytelność, nie wpływają na logikę.

Przykład ściągawkowy:

expect(object)
.to.be.an('array')
.with.lengthOf(2);

Odpowiednik w assert wymaga dwóch asercji:

assert.isTrue(Array.isArray(object));
assert.equal(object.length, 2);

Przykład z Mocha

const expect = require('chai').expect;
const sum = require('../src/sum');

describe('sum() – styl expect', () => {
it('powinno zwrócić poprawną sumę', () => {
const result = sum(2, 3);
expect(result).to.equal(5);
});

it('powinno zwrócić liczbę', () => {
const result = sum(2, 3);
expect(result).to.be.a('number');
});
});

Zalety stylu expect

  • naturalny, „ludzki” język – testy czyta się jak specyfikację zachowania;
  • łańcuchowe asercje – w jednym wyrażeniu sprawdzisz typ, długość, wartości i własności;
  • brak modyfikacji prototypu – technicznie bezpieczny, jak assert;
  • szerokie wsparcie komunikatów – łatwo doprecyzować błąd w razie niepowodzenia;
  • domyślny wybór – najczęściej polecany jako najbardziej elastyczny styl.

Wady stylu expect

Minimalnie więcej „magii” — początkujący muszą zrozumieć, że słowa łańcucha są pomocnikami składniowymi.

Krótka krzywa nauki — dla osób przyzwyczajonych do czystego assert składnia BDD wymaga chwili przyzwyczajenia.

Styl should — BDD z modyfikacją prototypu

Charakterystyka

Styl should dodaje właściwość should do Object.prototype po jednorazowej inicjalizacji.

var should = require('chai').should();

var foo = 'bar';
foo.should.be.a('string');
foo.should.equal('bar');

Przykład w testach Mocha:

const should = require('chai').should();
const sum = require('../src/sum');

describe('sum() – styl should', () => {
it('powinno zwrócić poprawną sumę', () => {
const result = sum(2, 3);
result.should.be.a('number');
result.should.equal(5);
});
});

Składnia jest zbliżona do expect, ale punktem wyjścia jest obiekt: zamiast expect(result) piszesz result.should.

Najważniejsza różnica techniczna

W przeciwieństwie do assert i expect, styl should modyfikuje Object.prototype, dodając do każdego obiektu właściwość should.

W środowiskach z kodem współdzielonym lub bibliotekach lepiej wybrać assert/expect, aby unikać globalnych modyfikacji i potencjalnych konfliktów.

Możliwy konflikt nazw — jeśli inna biblioteka zakłada „czysty” prototyp lub używa tej samej nazwy, mogą pojawić się problemy integracyjne.

Inne różnice

Ograniczone komunikaty błędów — w should trudniej dodawać własne komunikaty niż w assert/expect.

Wysoka czytelność kontra ryzyko modyfikacji prototypu — część zespołów unika should z powodów technicznych.

Porównanie: assert vs expect vs should

Poniższa tabela zestawia kluczowe różnice trzech stylów:

Aspekt assert expect should
Styl TDD BDD BDD
Składnia assert.equal(a, b) expect(a).to.equal(b) a.should.equal(b)
Łańcuchowość raczej nie tak, rozbudowane łańcuchy tak, rozbudowane łańcuchy
Modyfikacja Object.prototype nie nie tak — dodaje should
Czytelność „naturalnym językiem” średnia wysoka wysoka
Własne komunikaty szerokie wsparcie szerokie wsparcie ograniczone
Integracja z istniejącym kodem zbliżony do Node assert popularny, rekomendowany wymaga ostrożności
Typowe zastosowanie proste testy, integracja z Node większość testów aplikacyjnych projekty świadomie akceptujące modyfikację prototypu

Który styl wybrać w projekcie?

Pod względem możliwości wszystkie style potrafią to samo — decyduje ergonomia i wymagania techniczne projektu.

Praktyczne rekomendacje:

  • Nowy projekt aplikacyjny – wybierz expect dla czytelności i elastyczności bez modyfikacji prototypu;
  • Biblioteka lub kod niskopoziomowy – postaw na assert lub expect, unikaj should z uwagi na Object.prototype;
  • Spójność zespołu – ważniejsze od stylu jest konsekwentne stosowanie jednego podejścia w całym repozytorium;
  • Integracja z kodem w stylu Node assert – naturalnym wyborem bywa assert z bogatszym API Chai.

Przykłady testów z Mocha i Chai (trzy style)

Załóżmy prostą funkcję sprawdzającą e-mail:

// src/isValidEmail.js
function isValidEmail(email) {
return typeof email === 'string' && email.includes('@');
}
module.exports = isValidEmail;

Test w stylu assert

// test/isValidEmail.assert.spec.js
const assert = require('chai').assert;
const isValidEmail = require('../src/isValidEmail');

describe('isValidEmail – styl assert', () => {
it('zwraca true dla poprawnego maila', () => {
assert.isTrue(isValidEmail('[email protected]'), 'oczekiwano true dla poprawnego emaila');
});

it('zwraca false dla niepoprawnego maila', () => {
assert.isFalse(isValidEmail('niepoprawny'), 'oczekiwano false dla niepoprawnego emaila');
});
});

Test w stylu expect

// test/isValidEmail.expect.spec.js
const expect = require('chai').expect;
const isValidEmail = require('../src/isValidEmail');

describe('isValidEmail – styl expect', () => {
it('zwraca true dla poprawnego maila', () => {
expect(isValidEmail('[email protected]')).to.be.true;
});

it('zwraca false dla niepoprawnego maila', () => {
expect(isValidEmail('niepoprawny')).to.be.false;
});
});

Test w stylu should

// test/isValidEmail.should.spec.js
const should = require('chai').should();
const isValidEmail = require('../src/isValidEmail');

describe('isValidEmail – styl should', () => {
it('zwraca true dla poprawnego maila', () => {
isValidEmail('[email protected]').should.be.true;
});

it('zwraca false dla niepoprawnego maila', () => {
isValidEmail('niepoprawny').should.be.false;
});
});

Przykład — testy asynchroniczne (np. żądania HTTP)

Chai świetnie współpracuje z Mocha w testach asynchronicznych — zarówno z callbackami, jak i async/await.

Przykładowa funkcja pobierająca dane:

// src/fetchUser.js
async function fetchUser(id) {
// tu mógłby być fetch/axios
return { id, name: 'Ada', role: 'admin' };
}
module.exports = fetchUser;

Test w stylu expect z async/await:

// test/fetchUser.expect.spec.js
const expect = require('chai').expect;
const fetchUser = require('../src/fetchUser');

describe('fetchUser – styl expect', () => {
it('zwraca obiekt użytkownika z poprawnymi polami', async () => {
const user = await fetchUser(1);
expect(user).to.be.an('object');
expect(user).to.have.property('id', 1);
expect(user).to.have.property('name').that.is.a('string');
expect(user).to.have.property('role').that.is.oneOf(['admin', 'editor', 'viewer']);
});
});

Łańcuchowe API ułatwia opisanie wymaganych właściwości obiektu w jednym, czytelnym miejscu.

Przydatne asercje i łańcuchy w Chai

Różnica equal vs deep.equal

equal (odpowiednik ===) — porównuje referencje/prymitywy, sprawdzając, czy to dokładnie ta sama wartość/ten sam obiekt.

expect(obj1).to.equal(obj2); // sprawdza, czy to ten sam obiekt

deep.equal / eql — porównuje strukturę i wartości (głębokie porównanie).

expect(obj1).to.deep.equal(obj2); // sprawdza, czy obiekty „wyglądają” tak samo

Chai dokumentuje expect(object).to.equal(expected) oraz expect(object).to.eql(expected)/to.deep.equal(expected) dla porównań głębokich.

Sprawdzanie typu, długości, zawartości

Poniżej szybka ściąga najczęstszych asercji:

// typ
expect('foo').to.be.a('string');
expect([1, 2, 3]).to.be.an('array');

// długość
expect('foo').to.have.lengthOf(3);
expect([1, 2, 3]).to.have.lengthOf(3);

// zawieranie
expect([1, 2, 3]).to.include(2);
expect({ role: 'admin', name: 'Ada' }).to.have.property('role').that.equals('admin');

// negacja
expect('foo').to.not.equal('bar');
expect([]).to.be.empty;
expect('tekst').to.not.be.empty;

Słowa .to, .be, .have, .with, .and poprawiają czytelność i mogą być łączone — same w sobie nie wykonują logiki.

Chai w testach dostępności frontendu

Chai nie jest „silnikiem dostępności”, ale doskonale sprawdza się w asercjach zachowania interfejsu pod kątem dostępności — zwłaszcza z narzędziami DOM (np. JSDOM, test runner w przeglądarce).

Przykład — atrybuty ARIA i role

Wyobraźmy sobie komponent przycisku:

<button id="menu-toggle" aria-expanded="false" aria-controls="main-menu">
Menu
</button>

W testach (z DOM w środowisku testowym) możesz sprawdzić:

const expect = require('chai').expect;

describe('Menu toggle – dostępność', () => {
it('powinien mieć poprawne atrybuty ARIA', () => {
const button = document.getElementById('menu-toggle');
expect(button).to.exist;
expect(button.getAttribute('aria-controls')).to.equal('main-menu');
expect(button.getAttribute('aria-expanded')).to.be.oneOf(['true', 'false']);
});
});

Tym sposobem potwierdzasz, że interaktywny element ma krytyczne atrybuty dla użytkowników technologii asystujących.

Przykład — stan focusu i obsługa klawiatury

Element powinien otrzymywać focus z klawiatury (Tab) oraz reagować na Enter/Space, jeśli pełni rolę przycisku.

it('powinien być fokusowalny z klawiatury', () => {
const button = document.getElementById('menu-toggle');
button.focus();
expect(document.activeElement).to.equal(button);
});

Chai weryfikuje stan DOM — to wystarcza do testów regresyjnych dostępności (np. czy nie zniknęły ważne atrybuty po refaktoryzacji).

Dobre praktyki pracy z Chai (assert, expect, should)

Stosuj poniższe zasady, by testy były czytelne i stabilne:

  • Wybierz jeden styl jako domyślny – najczęściej expect w aplikacjach i assert w prostych bibliotekach;
  • Dbaj o spójność – nie mieszaj stylów w jednym pliku/teście bez wyraźnej potrzeby;
  • Unikaj should w bibliotekach – modyfikacja Object.prototype może powodować subtelne konflikty;
  • Dodawaj czytelne komunikaty błędów – szczególnie w assert/expect, by przyspieszyć debugowanie;
  • Wykorzystuj łańcuchy – np. expect(user).to.be.an('object').with.property('name').that.is.a('string'); dla zwięzłości i jasności;
  • Używaj deep.equal do struktur – obiekty i tablice porównuj głęboko, unikniesz pułapki porównania referencji;
  • Testuj dostępność razem z logiką – te same narzędzia i styl asercji obniżają próg wejścia dla zespołu;
  • Rozważ wtyczki Chai – ekosystem rozszerzeń (np. dla obietnic, integracji z Sinon) upraszcza testowanie złożonych scenariuszy.