Обещания становятся общей частью кода 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);
Но если value
eventually
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!');
Тестовые крючки
Вы можете использовать обещания в тестовых хуках так же, как и в любой другой тестовой функции. Это работает с before
after
beforeEach
afterEach
Например:
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
Предоставляет вспомогательные функции, resolves
rejects
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 не узнает об этом, и ваш тест может молча провалиться, не сообщив вам об этом.
Как я уже упоминал во введении, я создал пример проекта, который вы можете загрузить с моего веб-сайта, который показывает методы, представленные в этой статье. Не стесняйтесь загружать это и играть с этим.