Unit-тесты с асинхронностью в Jasmine

Продолжая тему тестирования, давайте разберёмся, как в Jasmine работать с асинхронностью. При разработке клиентской части — это, на мой взгляд, достаточно редкий случай тестов, но знать всё равно будет полезно! Вообще, в unit-тестах при помощи mock-ов желательно избавляться от асинхронности, чтоб гарантировать один из критериев unit-тестирования — изолированность модуля от другой части приложения и высокая скорость работы тестов. Но если вдруг вам всё-таки нужно протестировать функцию, то нам на помощь придёт стандартный механизм BDD-фрэймворка Jasmine.

Не все разработчики при написании спецификаций помнят или знают о том, что функции, отдаваемые в beforeAll, afterAll, beforeEach, afterEach и it, могут получать необязательный аргумент-функцию, которая должна быть вызвана сразу после завершения асинхронного действия. После этого Jasmine продолжит исполнение.

Обычно этот аргумент называют done, но это не обязательно, название может быть произвольным. Но если вы определили аргумент, но не вызвали эту функцию в своём коде, то вы получите ошибку «Error: Timeout — Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.», а набор спецификаций пойдёт выполняться дальше. Простой пример удачной проверки асинхронного события:

it('should complete after a second', function(done) {
	setTimeout(function () {
		done();
	}, 1000);
});

Для примера мы взяли самый простой способ получения асинхронного события в JS — определили таймаут.

К слову о таймауте ожидания асинхронного события. По умолчанию Jasmine будет ждать вызова функции done 5 секунд, после чего завершит тест с ошибкой. Для явного большинства тестов с асинхронностью этого будет достаточно. Но! Если вдруг вам для какого-то определённого теста или набора тестов и этого таймаута не хватает (!!!) или вы наоборот хотите максимальный таймаут ожидания асинхронного результата хотите уменьшить, вы можете последним параметром в эти функции отдать максимальный таймаут (это работает для всех вышеперечисленных функций: beforeAll, afterAll, beforeEach, afterEach, it). Пример:

it('should run less than a second', function(done) {
	setTimeout(function () {
		done();
	}, 900);
}, 1000);

Тут мы проверяем, что вызов функции done произойдёт меньше, чем через секунду. Если мы вместо 900 напишем 1000 или больше, тест выдаст ошибку. Таким образом вы можете лимитировать длительность ожидания асинхронной реакции как в большую, так и в меньшую сторону.

Для того, чтоб для определённого набора тестов не менять длительность таймаута у каждого, а задать его глобально для набора, таймаут по умолчанию можно глобально для набора переопределить через jasmine.DEFAULT_TIMEOUT_INTERVAL. Сделать это можно как-то так:

... Specs List ...
describe('shirt async suite', function () {
	var originDefaultTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
	beforeAll(function () {
		jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
	});
	afterAll(function () {
		jasmine.DEFAULT_TIMEOUT_INTERVAL = originDefaultTimeout;
	});
	it('should fail after a second expectation', function(done) {
		setTimeout(function () {
			done();
		}, 2000);
	});
});
it('should complete after 2 seconds timeout', function(done) {
	setTimeout(function () {
		done();
	}, 2000);
});
... Specs List ...

Если же есть необходимость сообщить об ошибке выполнения или о негативном завершении выполнения теста до того, как истечёт таймаут, можно вызвать метод done.fail. В эту функцию можно передать строку, описывающую причину ошибки. Пример использования такой:

it('should fail with reason after timeout', function(done) {
	setTimeout(function () {
		done.fail('Всё сломалось!');
	}, 100);
});

Вот и всё по этой теме! Ещё раз замечу, что это достаточно редкий случай, но если вам вдруг придётся писать такие тесты, я надеюсь, что мои объяснения с примерами помогут вам в решении поставленных задач и ни одна строка из под вашего «пера» не выйдет не протестированной :) Обязательно пишите тесты и жизнь ваша станет радостнее! :)

До новых статей!