Статьи

Обещания в модульных тестах JavaScript: полное руководство

Обещания становятся общей частью кода JavaScript. Собственный объект Promise

Несмотря на то, что сделать асинхронный код проще, работа с обещаниями в модульных тестах становится проблемой. Вам необходимо связать утверждения вашего теста с обратными вызовами обещания, что добавляет дополнительный код в тест. Таким образом, сам тест становится немного сложнее, и становится сложнее увидеть, что происходит.

В этой статье я покажу вам, как решить эту проблему, и расскажу о полезных шаблонах, которые могут упростить общие сценарии обещаний на стадии тестирования.

Я создал пример проекта, который вы можете скачать с моего сайта, который показывает методы, представленные в этой статье.

Начиная

Для этого проекта я буду использовать Mocha в качестве среды тестирования и библиотеку Chai для предоставления утверждений. Вы поймете, почему через мгновение.

Мы можем установить дуэт, просто запустив команду:

 npm install mocha chai

Когда вы впервые сталкиваетесь с обещаниями в модульных тестах, ваш тест, вероятно, выглядит как типичный модульный тест:

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

it('should do something with promises', function(done) {
  //define some data to compare against
  var blah = 'foo';

  //call the function we're testing
  var result = systemUnderTest();

  //assertions
  result.then(function(data) {
    expect(data).to.equal(blah);
    done();
  }, function(error) {
    assert.fail(error);
    done();
  });
});

У нас есть некоторые тестовые данные, и мы вызываем тестируемую систему — кусок кода, который мы тестируем. Но затем появляется обещание, и код усложняется.

Для обещания мы добавляем два обработчика. Первый предназначен для разрешенного обещания, в котором есть утверждение для сравнения равенства, а второй — для отклоненного обещания, которое имеет ошибочное утверждение. Нам также нужны вызовы done() Поскольку обещания асинхронны, мы должны сообщить Mocha, что это асинхронный тест, и уведомить его, когда он будет выполнен.

Но зачем нам assert.fail Цель этого теста — сравнить результат успешного обещания со значением. Если обещание отклонено, тест не пройден. Вот почему без обработчика ошибок тест может дать ложный результат!

Ложный положительный результат — это когда тест должен провалиться, но на самом деле это не так. Например, представьте, что мы удаляем обратный вызов отклонения. Ваш код должен выглядеть так:

 result.then(function(data) {
  expect(data).to.equal(blah);
  done();
});

В этом случае, если обещание было отклонено, ошибки не будет, поскольку в тесте нет обработчика ошибок, который бы его проверял. Но ясно, что тест должен провалиться в этой ситуации, так как ожидание не будет выполнено. Это определенно одна из главных причин, почему обещания усложняются в тестах.

Мокко и обещания

Я решил использовать Mocha в этом проекте, потому что он имеет встроенную поддержку для обещаний. Это означает, что отклоненное обещание провалит ваш тест. Например:

 it('should fail the test', function() {
  var p = Promise.reject('this promise will always be rejected');
  
  return p;
});

Приведенный выше тест возвращает отклоненное обещание, что означает, что оно каждый раз не выполняется. Мы можем использовать то, что узнали, чтобы улучшить наш предыдущий тест, как показано в следующем фрагменте:

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

it('should do something with promises', function() {
  var blah = 'foo';

  var result = systemUnderTest();

  return result.then(function(data) {
    expect(data).to.equal(blah);
  });
});

Тест теперь возвращает обещание. Нам больше не нужен обработчик сбоев или обратный вызов done Если обещание не выполнится, Мокко не пройдёт тест.

Улучшение тестов дальше с Chai-как-обещано

Разве не было бы неплохо, если бы мы могли делать утверждения непосредственно по обещаниям? С Чай-как-обещали мы можем!

Во-первых, нам нужно установить его работающим:

 npm install chai-as-promised

Мы можем использовать это так:

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

var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);

it('should do something with promises', function() {
  var blah = 'foo';

  var result = systemUnderTest();

  return expect(result).to.eventually.equal(blah);
});

Мы заменили всю then Ключ здесь в eventually Сравнивая значения с Чайем, мы можем использовать

 expect(value).to.equal(something);

Но если valueeventually

 return expect(value).to.eventually.equal(something)

Теперь Чай имеет дело с обещанием.

Примечание: не забудьте вернуть обещание, иначе Мокко не узнает, что с ним нужно справиться!

Мы можем использовать любое из утверждений Чая вместе со eventually Например:

 //assert promise resolves with a number between 1 and 10
return expect(somePromise).to.eventually.be.within(1, 10);

//assert promise resolves to an array with length 2
return expect(somePromise).to.eventually.have.length(2);

Полезные шаблоны для обещаний в тестах

Сравнение объектов

Если разрешенное значение вашего обещания должно быть объектом, вы можете использовать те же методы для сравнения, что и обычно. Например, с помощью deep.equal

 return expect(value).to.eventually.deep.equal(obj)

Здесь действует то же предупреждение, что и без обещаний. Если вы сравниваете объекты, equal

Chai-as-обещано имеет удобный помощник для сравнения объектов:

return expect(value).to.eventually.become(obj)

Использование в eventually.become Вы можете использовать его для большинства сравнений на равенство с обещаниями — со строками, числами и т. Д. — если только вам не требуется сравнительное сравнение.

Утверждение против определенного свойства от объекта

Иногда вам может потребоваться проверить только одно свойство объекта из обещания. Вот один из способов сделать это:

 var value = systemUnderTest();

return value.then(function(obj) {
  expect(obj.someProp).to.equal('something');
});- var value = systemUnderTest().then(function(obj) {
  return obj.someProp;
});

return expect(value).to.eventually.equal('something');

Но, как и было обещано, есть альтернативный путь. Мы можем использовать тот факт, что вы можете связать обещания:

 var value = systemUnderTest()

return expect(value.then(o => o.someProp)).to.eventually.equal('something');

В качестве последней альтернативы, если вы используете ECMAScript 2015, вы можете сделать его немного чище, используя синтаксис жирной стрелки:

 Promise.all

Несколько обещаний

Если у вас есть несколько обещаний в тестах, вы можете использовать return Promise.all([
expect(value1).to.become('foo'),
expect(value2).to.become('bar')
]);

 return Promise.all([p1, p2]).then(function(values) {
  expect(values[0]).to.equal(values[1]);
});

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

Сравнение нескольких обещаний

Если у вас есть два (или более) обещания, которые нужно сравнить, можно использовать следующий шаблон:

 all

Другими словами, мы можем использовать then

Утверждение о неудачах

Иногда вы можете захотеть проверить, что определенный вызов делает обещание не выполненным, а не успешным. В этих случаях вы можете использовать rejected

 return expect(value).to.be.rejected;

Если вы хотите, чтобы отклонение сопровождалось определенным типом ошибки или сообщения, вы также можете использовать rejectedWith

 //require this promise to be rejected with a TypeError
return expect(value).to.be.rejectedWith(TypeError);

//require this promise to be rejected with message 'holy smokes, Batman!'
return expect(value).to.be.rejectedWith('holy smokes, Batman!');

Тестовые крючки

Вы можете использовать обещания в тестовых хуках так же, как и в любой другой тестовой функции. Это работает с beforeafterbeforeEachafterEach Например:

 describe('something', function() {
  before(function() {
    return somethingThatReturnsAPromise();
  });

  beforeEach(function() {
    return somethingElseWithPromises();
  });
});

Они работают аналогично тому, как обещания работают в тестах. Если обещание будет отклонено, Мокко выдаст ошибку.

Обещания и издевательства / окурки

Наконец, давайте посмотрим, как использовать обещания с заглушками. Я использую Sinon.JS для примеров ниже. Для этого вам необходимо установить его, выполнив команду:

 npm install sinon

Возвращение обещаний от окурков

Если вам нужен заглушка или макет, чтобы вернуть обещание, ответ довольно прост:

 var stub = sinon.stub();

//return a failing promise
stub.returns(Promise.reject('a failure'));

//or a successful promise
stub.returns(Promise.resolve('a success'));

Шпионить за обещаниями

Вы можете использовать шпионов в качестве обратных вызовов обещаний, как и другие функции, но это может оказаться бесполезным из-за асинхронности обещаний. Если вам нужно сделать утверждение против обещания, вам лучше сделать это, используя слово «как обещано».

 var spy = sinon.spy();
var promise = systemUnderTest();

promise.then(spy);

Sinon-в обещанном

Чтобы немного упростить окурки и обещания, мы можем использовать sinon-as-обещано. Может быть установлен через npm:

 npm install sinon-as-promised

Предоставляет вспомогательные функции, resolvesrejects

 var sinon = require('sinon');

//this makes sinon-as-promised available in sinon:
require('sinon-as-promised');

var stub = sinon.stub();

//return a failing promise
stub.rejects('a failure');

//or a successful promise
stub.resolves('a success');

Выводы

Обещания могут упростить наш асинхронный код и даже могут упростить асинхронные тесты — при условии, что вы добавите несколько полезных библиотек в смесь.

Встроенная поддержка обещаний Mocha в сочетании с Chai и chai-as-обещает упрощает тестирование кода, возвращающего обещание. Добавьте в смесь SinonJS и sinon-as-обещанный, и вы также можете легко их заглушить.

Помните одну важную вещь: при использовании обещаний в ваших тестах всегда возвращайте обещание из теста , иначе Mocha не узнает об этом, и ваш тест может молча провалиться, не сообщив вам об этом.

Как я уже упоминал во введении, я создал пример проекта, который вы можете загрузить с моего веб-сайта, который показывает методы, представленные в этой статье. Не стесняйтесь загружать это и играть с этим.