Тестирование в JS становится все более распространенной практикой. Но с чего начать? Существует множество фреймворков которые предоставляют API для написания JS тестов.

Данная статья — это краткий обзор различий между двумя популярными фреймворками для тестирования JS: Jasmine 2 и Mocha. Мы также обсудим наиболее полулярные библиотеки Chai и Sinon которые часто используются в связке с Jasmine и Mocha.

1. API (application programming interface)


API в Jasmine и Mocha очень схожи. Они оба поддерживают написание тестов используя BDD (Behavior Driven Development) подход. Вы можете спросить: «что такое BDD»? Если кратко, это подход к написанию тестов, который предоставляет возможность описания функциональности на разговорном языке.

describe('calculator', function() {
  describe('add()', function() {
    it('should add 2 numbers together', function() {
      // assertions here
    });
  });
});


Утверждения (assertions), или ожидания(expectations), как их часто называют, различаются в представленых фреймворках. Mocha не имеет встроеной assertion библиотеки. Существует несколько вариантов для использования в среде Node.js и для браузеров: Chai, should.js, expect.js, and better-assert. Большинство разработчиков выберают Chai в качестве assertion библиотеки. Так как ни одной из assertion библиотек нет в поставке с Mocha, вам нужно будет подключить ее в вашу тестовую среду. В Chai сужествует три типа assertions:

1) should (должен)
2) expect (ожидать)
3) assert (утверждать)

Тип expect аналогичен expect который предоставляет нам фреймворк Jasmine. Например если вы хотите написать проверку метода add и ваше ожидание что calculator.add(1, 4) будет равняться 5, то данная проверка будет аналогично выглядить используя как Jasmine так и Chai.

//Jasmine
expect(calculator.add(1, 4)).toEqual(5);


//Chai
expect(calculator.add(1, 4)).to.equal(5);

Очень похоже, верно? Если вы переходите с Jasmine на Mocha то путь довольно простой — использывать Chai c типом expect.

2. Test Doubles (Дублеры)


Test Doubles заменяют один обьект на другой для тестовых целей. В Jasmine роль test doubles выполняют spies(шпионы). Spy — это функция которая заменет оригинальную функцию, логику которой мы хотим изменить и описывает как данная функция будет использоваться в рамках выполнения теста.

Spies позволяют:

1) Узнать количество раз которые вызывалась spy функция
2) Указать возвращаемое значение для того, чтобы тестируемый код продолжил работать по нужному алгоритму.
3) Указать, чтобы spy функция бросила ошибку.
4) Узнать с какими аргументами была вызвана spy функция.
5) Указать, чтобы spy функция вызвала оригинальную функцию. (Которую она заменила)

В Jasmine создать spy для существующего метода возможно так:

var userSaveSpy = spyOn(User.prototype, 'save');

Также возможно создать spy, даже если у вас нет метода который вы хотите подменить.

var spy = jasmine.createSpy();

Mocha не имеет встроеной test doubles библиотеки по этому вам нужно загрузить и подключить Sinon в вашу тестовую среду. Sinon очень мощная Test Doubles библиотека которая предоставляет эквивалентный функционал spies в Jasmine и немного больше. Стоит отметить, что Sinon разбивает test doubles на три разные категории: spies, stubs и mocks, между которыми есть тонкие отличия.

Spy функция в Simon вызывается посредством оригинального метода, в то время как в Jasmine это поведение нужно указывать. Например:

spyOn(user, 'isValid').andCallThrough() // Jasmine
// is equivalent to
sinon.spy(user, 'isValid') // Sinon

В данном примере, будет вызван оригинальный user.isValid.

Следующий тип test doubles это stubs который заменяет оригинальный метод. Поведение stubs аналогично с поведением по умолчанию spies в Jasmine, в котором оригинальный метод не вызывается.

sinon.stub(user, 'isValid').returns(true) // Sinon
// is equivalent to
spyOn(user, 'isValid').andReturns(true) // Jasmine

В данном примере, если будет вызван метод user.isValid, оригинальный метод user.isValid вызван не будет, а будет вызвана его поддельная версия которая должна вернуть результат «true».

Из своего опыта, spies в Jasmine покрывают почти все что требуется для test doubles, по этому в большинстве случаев вам не нужно будет Sinon если вы используете Jasmine, однако, если вы хотите, у вас есть возможность использовать их совместно. Основная причина, по которой я использую Sinon вместе с Jasmine, это его fake server.

3. Асинхронные тесты (Asynchronous Tests)


Асинхронное тестирование в Jasmine 2.x и Mocha реализуется аналогично.

it('should resolve with the User object', function(done) {
  var dfd = new $.Deferred();
  var promise = dfd.promise();
  var stub = sinon.stub(User.prototype, 'fetch').returns(promise);

  dfd.resolve({ name: 'David' });

  User.get().then(function(user) {
    expect(user instanceof User).toBe(true);
    done();
  });
});

В данном примере User — функция конструктор у которой есть статический метод get. Метод get внутри себя использует метод fetch который выполняет XHR запрос (request). Я хочу проверить, что когда метод get получит значение, то это значение будет экземпляром (instance) User. Так как я использовал «stub» для метода User.prototype.fetch и указал ему вернуть заранее определенный promise, реальный XHR запрос не выполняется. Покрытие данного кода продолжает быть асинхронным.

Очень просто указать, что callback функция в it конструкции ожидает аргумент (в данном случаи done) и test runner будет ожидать пока выполнится функция до того как закончит тест. Тест будет приостановлен и выводится ошибка, если аргумент не будет вызван в течении определенного времени. Это дает полный контроль над тем, когда тесты закончат выполнение. Тест, написаный выше, будет работать как в Jasmine так и в Mocha.

Если вы работаете с Jasmine 1.3 асинхронное тестирование выглядит не так «чисто».

Пример асинхронного тестирования в Jasmine 1.3:

it('should resolve with the User object', function() {
  var flag = false;
  var david;

  runs(function() {
    var dfd = new $.Deferred();
    var promise = dfd.promise();

    dfd.resolve({ name: 'David' });
    spyOn(User.prototype, 'fetch').andReturn(promise);

    User.get().then(function(user) {
      flag = true;
      david = user;
    });
  });

  waitsFor(function() {
    return flag;
  }, 'get should resolve with the model', 500);

  runs(function() {
    expect(david instanceof User).toBe(true);
  });
});

В данном примере, для завершения асинхронной операции тест будет ожидать максимум 500 милисекунд, в ином случае тест будет провален. Функция waitsFor() постоянно проверяет flag, как только flag станет true выполнение продолжится и будет вызван следующий runs блок.

4. Sinon Fake Server


Одна особенность которую имеет Sinon в сравнении с Jasmine это fake server (поддельный сервер). Это позволяет установить поддельные ответы на определенные AJAX запросы.

it('should return a collection object containing all users', function(done) {
  var server = sinon.fakeServer.create();
  server.respondWith("GET", "/users", [
    200,
    { "Content-Type": "application/json" },
    '[{ "id": 1, "name": "Gwen" },  { "id": 2, "name": "John" }]'
  ]);

  Users.all().done(function(collection) {
    expect(collection.toJSON()).to.eql([
      { id: 1, name: "Gwen" },
      { id: 2, name: "John" }
    ]);

    done();
  });

  server.respond();
  server.restore();
});

В данном примере послав GET запрос на /users, мы получим ответ 200, содержащий данные о двух пользователях — Гвене и Джоне. Это может быть очень удобно по нескольким причинам. Во первых, вы можете тестировать код, который делает AJAX запросы, независимо от того, какую AJAX библиотеку вы используете. Во вторых, вы можете протестировать функцию, которая делает AJAX запрос и делает предварительную обработку возвращаемых данных перед тем, как выполнить обещание (resolve promise). В третьих, есть возможность обьявить несколько ответов на успешный запрос. Например, в случаях: успешного снятия средств с кредитной карты, устаревшей карты, неправильного CVV кода и т.д. прийдут разные ответы. Если вы работали с AngularJs, то Sinon Fake Server похож на $httpBackend сервис.

Итог:


Jasmine фреймворк включает в себя почти все что необходимо, включая assertions, test doubles (реализован через spies) функциональность. Mocha не обладает такой функциональностью, но предоставлет выбор библиотеки для assertions (самая популярная Chai). Для test doubles Mocha также требует подключения дополнительной библиотеки, в большинстве случаев это sinon.js. Sinon также может быть отличным дополнением, предоставляя свой fake server (поддельный сервер).

Выбрать тестовый фреймворк для JS может быть трудной задачей, надеюсь что данная статья помогла вам сделать правильный выбор. В любом случае, что бы вы ни использовали, вы не ошибетесь. Удачного тестирования!

Статья-оригинал
Поделиться с друзьями
-->

Комментарии (16)


  1. vintage
    11.11.2016 12:08
    -1

    Насчёт асинхронных тестов. Они получаются слишком хрупкими и запутанными. Поэтому вместо асинхронных тестов, я сейчас пишу синхронные вида:


    1. Выполнили какое-то действие, предварительно застабив внешние зависимости.
    2. "Промотали" время.
    3. Проверили результат.

    Соответственно, ошибки запроса вываливаются на первом шаге. Ошибки обработки ответа — на втором. Ошибки логики — на третьем. Тесты получаются простыми, понятными и быстрыми.


    1. VladimirZaets
      11.11.2016 14:51

      Я считаю что работа с таймерами является наоборот более хрупким решением т.к есть прямая зависимость на время («Промотали» время"), что может привести хоть и к редким но рандомным падениям.


      1. vintage
        11.11.2016 16:06

        Я ничего не говорил про таймеры :-) И "промотка" не зря в кавычках. Грубо говоря просто дёргается ручка "запустить следующую партию отложенных задач". Соответственно мок XHR в этом случае запустит обработчики ответа на запрос, предоставив им ответ. Мок Promise — следующий обработчик обещания и тд.


      1. Vadem
        11.11.2016 17:19
        +1

        Поддерживаю подход с прокруткой времени.
        Если замокать все API и таймеры, то тесты действительно получаются простыми, понятными и быстрыми.
        Я долго практиковал подход с done(), потом использовал RxJS. В итоге пришёл к простой промотке времени и остался доволен.


    1. 8gen
      11.11.2016 14:58

      И всё это с помощью sinon.useFakeTimers() или что-то другое?


      1. vintage
        11.11.2016 16:52
        -1

        Да мне пока хватает своего велосипеда. Пример теста:


                'wait for data'() {
        
                    class Test extends $mol_object {
        
                        @ $mol_mem()
                        source( next? : string , prev? : string ) : string {
                            new $mol_defer( () => {
                                this.source( void 0 , 'Jin' )
                            } )
                            throw new $mol_atom_wait( 'Wait for data!' )
                        }
        
                        @ $mol_mem()
                        middle() {
                            return this.source()
                        }
        
                        @ $mol_mem()
                        target() {
                            return this.middle()
                        }
        
                    }
        
                    const t = new Test
        
                    $mol_assert_fail( ()=> t.target().valueOf() , $mol_atom_wait )
        
                    $mol_defer.run()
        
                    $mol_assert_equal( t.target() , 'Jin' )
                } ,


    1. Sutar
      11.11.2016 17:09

      А можно попросить вас привести какой-нибудь простой пример?


  1. SirEdvin
    11.11.2016 12:13
    +1

    Тестирование в JS становиться все более распространенной практикой

    Я даже не знаю, это хорошо или печально. В плане, печально что только сейчас.


  1. Miraage
    11.11.2016 12:32
    +1

    А про Jest, судя по всему, люди даже не слышали.


    1. auine
      11.11.2016 12:52

      А Jest просто дешевые понты :)


      1. Miraage
        14.11.2016 09:52

        Ваша аргументация неоспорима.)


    1. justboris
      11.11.2016 12:54

      это перевод статьи 2015 года. Jest тогда еще не был так популярен.


      Но, хороший вопрос, зачем переводить такую старую неактуальную статью?


      1. VladimirZaets
        11.11.2016 14:27
        +1

        Тем не менее, Jasmine и Mocha сейчас актуально используются во многих проектах, и вопрос миграции с одного на другое вполне актуальный (по своей работе я сейчас с этим столкнулся). Именно по этому я решил перевести данную статья т.к она показывает основную схожость и основные различия API.


  1. k12th
    11.11.2016 15:02
    -1

    Только Tape. Хуже Jasmine с его неконсистентным API вообще ничего нет, даже QUnit был приятнее. А уж эти бесконечные глобалы, из-за которых надо костылить и линтеры, и IDE, брр!


  1. vladi_gurulev
    11.11.2016 20:49

    Подскажите, как работает assert для массивов. Внизу пример теста функции, которая разбивает массив на определенное число частей. Функция работает правильно, но тест выдает ошибку. Где косяк?

    describe ("breakArray", function() {
    	
    	describe("Разбиение массива на массивы заданной размерности", function() {
    		
    		it  ("Разбиение массива на массивы по 2 бит", function() {
    			assert.equal( breakArray([1, 2, 3, 4], 2), [[1, 2], [3, 4]] );
    		});
    	});
    	
    });
    		
    


    image


    1. qtuz
      11.11.2016 21:25

      для массивов и объектов нужно использовать assert.deepEqual