[Из песочницы] Jasmine vs. Mocha, Chai и Sinon

Тестирование в 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 togoether', 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 может быть трудной задачей, надеюсь что данная статья помогла вам сделать правильный выбор. В любом случае, что бы вы ни использовали, вы не ошибетесь. Удачного тестирования!

Статья-оригинал

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

  • 11 ноября 2016 в 12:08

    0

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


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

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

  • 11 ноября 2016 в 12:13 (комментарий был изменён)

    0

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

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

© Habrahabr.ru