Testowanie aplikacji internetowych
Mocha, Chai, Sinon
Bartosz Szczeciński
18.05.2018
@btmpl
medium.com/@baphemot
Mocha
Test Runner
Mocha
Mocha to tzw. "test runner" czyli oprogramowanie przeznaczone do definiowania środowiska testowania
i wykonywania samych testów.
Domyślnie Mocha pozwala na uruchamianie testów w przeglądarce, albo w środowisku Node (w takim wypadku może korzystać ono z przeglądarek typu PhantomJS, lub tworzyć wirtualne środowisko DOM używając jsdom).
Mocha - dodanie do strony
<!-- dodaj zasoby Mocha oraz przydatne biblioteki -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/mocha/5.1.1/mocha.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/5.1.1/mocha.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sinon.js/5.0.7/sinon.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.1.2/chai.js"></script>
<!-- dodaj węzeł, do którego wygenerowany zostanie raport -->
<div id="mocha"></div>
<!-- zainicjuj mochę w trybie bdd lub tdd -->
<script>
mocha.setup({
ui: 'bdd'
})
</script>
<!-- zaimportuj pliki z kodem -->
<!-- zdefiniuj swoje testy -->
<!-- uruchom testy -->
<script>
mocha.run();
</script>
Mocha - przykładowy test
Oferowane API
W zależności od trybu (BDD, TDD) mocha definiuje kilka składni (które są w większości tożsame - poniższe przykłady dostępne są w trybie BDD)
describe - opisuje logiczną grupę testów, dzieląc je na sekcje
it / test - opisuje pojedynczy test
before / after - pozwala na zdefiniowanie kodu pomocniczego, wywołanego przed / po wszystkich testach w grupie
beforeEach / afterEach - kod pomocniczy, wywoływany przed / po każdym z testów w grupie
Oferowane API
Za pomocą modyfikatorów możemy decydować, czy dany test powinien zostać wywołany lub pominięty.
describe.skip - dana grupa testów powinna zostać pominięta; informacja o pominięciu zostanie odnotowana
test.skip / it.skip / xtest / xti - dany test powinien zostać pominięty w aktualnym wywołaniu; informacja o pominięciu testu zostanie odnotowana
test() - zdefiniowanie testu bez ciała oznacza test jako pominięty; zwykle w ten sposób oznacza się test, który planujemy napisać w przyszłości
describe.only - uruchom tylko te grupę testów
it.only - wykonaj tylko ten test
Można zdefiniować kilka testów jako .only i wszystkie one zostaną wywołane.
Oferowane API
Możemy także określać warunki zaliczenia testu:
this.timeout(ms) - domyślnie test oznaczony jest jako niewykonany po upływie 2000ms, istnieje możliwość zmiany (zwiększenie, zmniejszenie)
this.retries(int) - jeżeli wiemy, że test opiera się na zasobach zewnętrznych i może czasem nie przechodzić, możemy zadecydować, że powinien on być uruchomiony X razy
this.slow(ms) - mocha zaznacza testy, których wykonanie trwało zbyt długo; istnieje możliwość zmiany tego parametru
this.skip() - warunkowe pominięcie testu (np. na określonym środowisku; pomiń w Node, ale uruchom w przeglądarce, ponieważ nie mogliśmy zamockować danego WebAPI
Oferowane API
Mechanizmy do testowania kodu asynchornicznego
describe("Mechanizm pobierania danych", function() {
it("Pobiera dane z API i przekazuje do funkcji wywołującej", function(done) {
pobierzDane(function() {
// ten kod zostanie wykonany asynchronicznie
// za kilka milsekund
done();
})
})
});
Wywołanie done wielokrotnie oznacza nie tylko błąd w logice aplikacji, ale prowadzi do niestabilności testów!
Funkcjonalność
Mocha pozwala na różne reportery (generowanie podsumowania):
Funkcjonalność
Mocha nie oferuje żadnych narzędzi do assercji (assert, except, should) ale za to pozwala na pracę z wieloma standardowymi narzędziami, np ...
Chai
Assertion Library
Chai
Chai dostarcza nam kilka narzędzi pozwalających na weryfikację założeń biznesowych za pomocą składni TDD lub BDD.
BDD: w tym trybie dostępne są funkcje should()
i expect() które przyjmują badany obiekt i następnie mogą być odpytywane przez zastosowanie łańcucha metod w celu weryfikacji założeń biznesowych
TDD: w tym trybie możemy użyć obiektu assert, który udostępnia metody sprawdzające przekazany obiekt; assert nie udostępnia łańcucha, wszystkie założenia znane są "z góry"
Expect
expect = chai.expect;
expect(1).to.equal(1);
var result = someAssertion();
expect(result).to.be.true;
var badFn = function () {
throw new TypeError;
};
expect(badFn).to.throw();
var person = {
name: 'Bartek',
animals: ['Cat']
};
expect(person).to.have.property('animals').with.lengthOf(1);
Should
should = chai.should(); // wywołanie funkcji!
'Bartek'.should.be.a('string');
var person = {
name: 'Bartek',
animals: ['Cat']
};
person.should.have.property('animals').with.length(1)
Should
Should modyfikuje prototype Object.prototype dodając do niego odpowiednie funkcje chai.
Z tego powodu nie nadaje się do pracy z elementami które nie są obiektami lub nie udostępniają API obiektów: null, boolean, undefined
Sinon
Spies, Mocks, Stubs, Fakes!
Spy
Spy przydane są w momencie, w którym chcemy obserwować interakcję z istniejącymi obiektami (np. funkcje) - sprawdzać czy, ile razy, z jakimi wartościami zostały wywołane etc. nie modyfikując ich wewnętrznej funkcjonalności.
var Person = {
test: function() {
console.log('Funkcja test została wywołana!');
return 1;
}
}
var spy = sinon.spy(Person, 'test');
Person.test(); // 'Funkcja test została wywołana'
expect(spy.called).to.equal(true);
spy.restore();
Użyj Spy jeżeli chcesz zweryfikować, że coś zostało wywołane w aplikacji oraz w jaki sposób.
Spy
Większość pracy ze Spy odbywa się poprzez weryfikację atrybutu .called i jego wielu wariantów.
.called - boolean, oznaczający, czy dana funkcja została wywołana czy też nie
.calledOnce, .calledTwice, .calledThrice, .callCount - pomocne w określeniu ile razy wystąpiło wywołanie
.calledWith(arg1, arg2, ...) - czy nastąpiło wywołanie z podanymi argumentami
.threw([string]) - czy wywołanie spowodowało rzucenie wyjątku (danego typu)
Stub
Użyteczne zamiast Spy w przypadku, kiedy chcemy zapobiec wywołaniu faktycznej metody (np. jest ona destruktywna, wywołuje liczne inne funkcje czy też powoduje odwołanie REST i nie chcemy go mockować).
Stub nie musi być w centrum testu - możesz wciąż testować logikę przy użyciu Spy, ale używać Stub żeby podstawić funkcjonalność API wewnątrz testowanej części aplikacji.
Użyj Stub, jeżeli musisz zmienić implementację części testowanej aplikacji na potrzeby testu.
Stub
Domyślnie stub nie zwraca żadnej wartości, możemy natomiast samodzielnie określić jego zachowanie używając potężnego narzędzia konfiguracji:
var callback = sinon.stub();
callback.withArgs(42).returns(1);
callback.withArgs(1).throws("name");
callback(); // undefined
callback(42); // 1
callback(1); // rzucony wyjątek Error
Stub
Możemy także użyć prostszy sposób notacji zwrotu:
var callback = sinon
.stub(MojeApi, "mojaFunkcja")
.callsFake(function (input) {
return input * 2;
});
Stub
var Person = {
test: function() {
console.log('Funkcja test została wywołana!');
return 1;
}
}
var stub = sinon.stub(Person, 'test');
Person.test(); // oryginalna funkcja nie jest wywoływana
expect(spy.called).to.equal(true);
stub.restore();
Stub
var Person = {
name: '',
age: '',
purge: function() {
if(WewnetrzneApi.UsunWszystkieDaneZBazy()) {
return this.usunDaneUzytkownika();
}
},
usunDaneUzytkownika: function() {
this.name = undefined;
this.age = undefined;
}
}
var stub = sinon.stub(WewnetrzneApi, 'UsunWszystkieDaneZBazy').returns(true);
var spy = sinon.spy(Person, 'usunDaneUzytkownika');
Person.purge();
expect(spy.called).to.equal(true);
stub.restore();
spy.restore();
Stub
.onCall(int) - rozpoczyna łańcuch opcji, dotyczących tylko n-tego wywołania
.withArgs() - dalsza część łańcucha dotyczy wywołań z konkretnymi argumentami
.returns(), .returnsArg(index), .returnsThis - zwraca określone dane
.throws() - rzuca wyjątek
.callThrough() - wywołuje oryginalną funkcję
Stub
Musimy pamiętać że wszystkie utworzone stuby, które zastępują implementacje na obiektach muszą zostać usunięte po zakończeniu testu, w innym wypadku będą one aktywne także w kolejnych testach:
it("pobiera posty z serwera", function() {
var callback = sinon.stub(window, "loadPosts");
pobierzPosty(); // wewnętrznie wywołuje stubowaną wersję loadPosts
expect(loadPosts.called).to.equal(true);
// callback.restore();
});
it("obsługuje błąd podczas pobierania postów", function() {
pobierzPosty(); // znowu wywołuje stub!"
});
Mock
Mock są zaawansowanym połączeniem Spy i Stub pozwalającym (albo wręcz wymuszającym) "udawanie" całych obiektów. Należy używać ich z rozwagą, ponieważ ich użycie może prowadzić do podmiany zbyt dużej części aplikacji, co spowoduje zmianę testowania logiki na testowanie implementacji testu.
Mocki jako jedyne posiadają wbudowany mechanizm assercji.
Mock
var daneWejsciowe = {
name: 'test',
admin: true
};
var daneWyjsciowe = {
name: daneWejsciowe.name,
nameLowercase: daneWejsciowe.name.toLowerCase()
};
var database = sinon.mock(UserDataStore);
// Wewnętrzne wywołanie konwersji nazwy użytkownika na slug
database
.expects('generateCanonicalName')
.once()
.withArgs(daneWejsciowe.name)
.returns(daneWejsciowe.name.toLowerCase());
// Zapisanie użytkownika do bazy
database.expects('save').once().withArgs(daneWyjsciowe);
// Do ustalenia praw administratora nie wystarczy podanie admin:true!
database.expects('setAdminRights').never();
MojeApi.dodajUzytkownika(daneWejsciowe);
database.verify();
database.restore();
Symulowane timery
Często zachodzi potrzeba przetestowania funkcji, która zawiera w sobie setTimeout lub setInterval; jeżeli jest to mała wartość, można potraktować test jako asynchroniczny, jeżeli jest znaczna, lepiej użyć fake timers:
function wywolajCallbackPoCzasie(callback) {
setTimeout(function() {
callback.call();
}, 10000);
}
test("wywołuje callback po upłynięciu 10.000 ms", function() {
var clock = sinon.useFakeTimers();
var stub = sinon.stub();
wywolajCallbackPoCzasie(stub);
clock.tick(11000);
expect(stub.called).to.equal(true);
});
Symulowane XMLHttpRequest
Dodatkowo, Sinon może symulować XHR na potrzeby testów:
var requests;
beforeEach(function() {
xhr = sinon.useFakeXMLHttpRequest();
requests = this.requests = [];
xhr.onCreate = function (xhr) {
requests.push(xhr);
};
});
it('get helper passes back backend response', function() {
var callback = sinon.fake();
var data = { status: 'OK', items: [{id: 1, title: 'Test'}]};
data = JSON.stringify(data);
get('//example.com', callback);
requests[0].respond(200,{
"Content-Type": "application/json"
}, data);
expect(callback.calledWith(data)).to.equal(true);
});
Ćwiczenie
Napisz testy dla mechanizmu routera:
- jeżeli zostanie on wywołany z wartością w postaci /test/1 to funkcja loadPost powinna zostać wywołana z wartością 1
- jeżeli zostanie on wywołany bez wartości, to funkcja loadIndex powinna zostać wywołana
Ściągawka: stub = sinon.stub(object, 'method') / stub.called / stub.calledWith(args)
Ćwiczenie
Napisz testy dla funkcji get i post
- get wywołane z adresem URL powinno wywołać callback przekazując do niego dane z serwera
- post powinno zmienić przekazany obiekt na string i ustawić odpowiednie nagłówki
Ściągawka: callbackStub.calledWith, requests[0].requestHeaders, requests[0].requestBody
Ściągawka: requests[0].respond(kodOdpowiedzi, obiektNaglowkow, odpowiedz)
Ćwiczenie
Napisz testy dla funkcji renderPosts i loadIndex
- powinna ona dodać maksymalnie 5 elementów do listy, która jest do niej przekazana
- klikanie na wyrenderowany element powinno zmieniać window.location.hash
- loadIndex powinno wywołać window.renderPosts z odpowiednią strukturą
Ściągawka: document.createElement, querySelectorAll('??').length, element.click(); musisz zamockować XHR
Testowanie aplikacji internetowych
By btmpl
Testowanie aplikacji internetowych
Wstęp do narzędzi Mocha, Chai i Sinon
- 410