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