
Jasmine на данный момент одна из самым популярных библиотек для организации юнит тестирования JavaScript.
На официальном сайте есть довольно хороший быстрый старт с примерами, но если все же хочется на русском и с комментариями – то добро пожаловать под кат.
Первым делом скачиваем себе библиотеку с офсайта. Далее создаем себе песочницу для экспериментов. Вместе с библиотекой сразу идет пример использования, но возможно сходу разобраться в нем будет не просто. Более простая версия будет выглядеть так:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="jasmine.css">
<script type="text/javascript" src="jasmine.js"></script>
<script type="text/javascript" src="tests.js"></script>
<script type="text/javascript">
var jasmineEnv = jasmine.getEnv();
jasmineEnv.specFilter = function(spec) {
console.log(spec);
};
window.onload = function() {jasmineEnv.execute();};
</script>
</head>
<body></body>
</html>
tests.js - файл, в котором мы опишем наши тесты
jasmine.getEnv - аналог синглтона для получения основного объекта
jasmineEnv.specFilter - callback для получения результатов
Сразу модуль для рендера результатов(jasmine-html.js) и подключим его. Итоговый код будет такой:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="jasmine.css">
<script type="text/javascript" src="jasmine.js"></script>
<script type="text/javascript" src="jasmine-html.js"></script>
<script type="text/javascript" src="tests.js"></script>
<script type="text/javascript">
var jasmineEnv = jasmine.getEnv();
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
window.onload = function() {jasmineEnv.execute();};
</script>
</head>
<body></body>
</html>
Песочница есть, теперь можем переходить к экспериментам в tests.js:
describe("Jasmine", function() {
it("makes testing JavaScript awesome!", function() {
expect(true).toBe(true);
});
});
describe - описание блока тестов
it - описание теста
* describe может включать в себя другой describe (если нужны подсекции)
далее идет сам тест:
expect(true).toBe(true);
понятное дело, что вместо true тут будет 2 выражения, которые должны быть равны. Какие еще могут быть варианты для сравнения?
expect(true).not.toBe(true);
expect(a).toEqual(b);
Отличие toEqual от toBe в том, что toBe – это строгое ссылочное сравнение (для объектов a = b = {} ), а в случае с toEqual – сравнение по содержимому( a = {}, b = {} ). Во втором случае toBe вернет fail.
Также возможны варианты для нахождение совпадений подстроки:
var message = 'foo bar baz';
expect(message).toMatch(/bar/);
expect(message).toMatch('bar');
expect(message).not.toMatch('bar');
Либо нахождения свойства в объекте:
var a = { foo: 'foo' };
expect(a.foo).toBeDefined();
expect(a.bar).not.toBeDefined();
Варианты проверки null и undefinded:
expect(a).not.toBeUndefined(); expect(b).toBeUndefined(); expect(a).toBeNull(); expect(b).not.toBeNull();
Аналогично работают методы: toBeTruthy, toBeFalsy, toContain(проверяет наличие значение в массиве), toBeLessThan, toBeGreaterThan, toBeCloseTo.
Если мы захотим проверить имя класса конструктора( аналог instanceof):
expect({}).toEqual(jasmine.any(Object));
expect(12).toEqual(jasmine.any(Number));
Также есть метод проверки исключений:
var foo = function() {
return 1 + 2;
};
var bar = function() {
return a + 1;
};
expect(foo).not.toThrow();
expect(bar).toThrow();
В группе тестов мы можем использовать инструкции beforeEach и afterEach, в которых мы указываем что нужно выполнить до/после каждого теста:
describe("A spec", function() {
var foo;
beforeEach(function() {
foo = new Object;
});
afterEach(function() {
foo = null; expect(bar).toBeNull();
});
it("first test", function() {
expect(foo).toEqual(1);
});
it("second test", function() {
expect(foo).toEqual(1);
});
Группы и тесты обладают интересной особенностью, если мы поставим перед describe или it символ x, т.е. xdescribe и xit, то они будут проигнорированы. (При этом ошибка о том, что метод не найден, вызвана не будет)
Еще с помощью Jasmine мы можем выставлять наблюдателей(observer) на разные объекты с помощью метода spyOn:
beforeEach(function() {
spyOn(foo, 'setBar');expect({}).toEqual(jasmine.any(Object)); expect(12).toEqual(jasmine.any(Number));
expect(bar).toBeNull();
foo.setBar(123);
foo.setBar(456, 'another param');
});
После чего станут доступны следующие проверки:
expect(foo.setBar).toHaveBeenCalled(); expect(foo.setBar.calls.length).toEqual(2); expect(foo.setBar).toHaveBeenCalledWith(123); expect(foo.setBar).toHaveBeenCalledWith(456, 'another param'); expect(foo.setBar.mostRecentCall.args[0]).toEqual(456); expect(foo.setBar.calls[0].args[0]).toEqual(123);
Для метода spyOn существует ряд надстроек:
spyOn(foo, 'getBar').andCallThrough(); // продолжаем выполнение основной функции
spyOn(foo, 'getBar').andReturn(745); // подменяем возвращаемое значение
spyOn(foo, 'getBar').andCallFake(function() {return 1001;}); // подменяем метод
и более сложные:
setBar = jasmine.createSpy('setBar');
foo = jasmine.createSpyObj('foo', ['setBar']);
По сути тоже самое что и spyOn(foo, ‘setBar’), только тут мы можем создавать наблюдатель на функцию(как в первом случае), а не на метод объекта и делать это динамически(формируя название функции).
В случае, когда мы должны произвести тест через некоторое время после запуска тестируемого кода, на помощь нам приходит метод jasmine.Clock( аналог setTimeOut):
var timerCallback;
beforeEach(function() {
timerCallback = jasmine.createSpy('timerCallback');
jasmine.Clock.useMock();
});
it("causes a timeout to be called synchronously", function() {
setTimeout(function() {
timerCallback(); // это функция выполнится через 100мс
}, 100);
expect(timerCallback).not.toHaveBeenCalled(); // еще не выполнилась
jasmine.Clock.tick(101); // ждем 101 мс
expect(timerCallback).toHaveBeenCalled(); // уже должна была выполниться
});
А сейчас разберем самое сложное место во всей библиотеке – асинхронные тесты, которые по моему мнению очень слабо разобраны в примерах на официальном сайте. Создаются они с помощью очередей организованных через методы runs() и waitsFor(). В метод runs() мы передаем наши асинхронные действия, а в waitsFor() прописываем условие, по которому мы узнаем что асинхронное действие завершено – обычно для этого используется глобальная переменная, как флаг. Допустим у нас есть асинхронный метод:
function asynchFunc(value, callback){
setTimeout(function() { //через 0.5с он
value++; // изменит значение
callback(value); // и вернет его через callback
}, 500);
}
Теперь напишем тест для него:
describe("Asynchronous specs", function() {
var value, flag;
it("async test", function() {
runs(function() {
flag = false; // начальные значения
value = 0;
asynchFunc(value, function(x){
value = x;
flag = true; // указывает на то что тест выполнен
});
});
waitsFor(function() {
return flag; // ждет когда флаг переключится в true
}, "Message for timeout case", 750);
// после выполняет следующий блок кода
runs(function() {
expect(value).toBeGreaterThan(0);
});
});
});
Метод waitsFor имеет следующий синтаксис:
waitsFor(conditionCallback, TimeOutMessage, TimeOut) conditionCallback - функция возвращающая true/false, она постоянно вызывается методом waitsFor пока не вернет true либо наступит TimeOut TimeOut - максимально время работы метода в мс TimeOutMessage - сообщение по приходу TimeOut
Итак еще раз: мы помещаем наши асинхронные действия в метод runs() и при выполнении этого действия меняем флаг, а в waitsFor() помещаем функцию, которая будет возвращать этот самый флаг. Причем тут может быть как один флаг, так и гораздо более сложные проверки (например, если у нас идет несколько асинхронных вызовов).
Полный пример в песочнице.
Желаю вам успешно пройденных тестов!