- PVSM.RU - https://www.pvsm.ru -
В продолжение предыдущей статьи о тестировании интерфейсов в Тинькофф Банке расскажу, как мы пишем unit-тесты на javascript.
Статей о подходах к тестированию TDD и BDD и так достаточно много, поэтому еще раз рассказывать подробнее об их особенностях не буду. Эта статья скорее для новичков или для разработчиков, которые только хотят начать писать тесты, но более опытные разработчики, возможно, тоже смогут найти для себя полезную информацию.
Сначала о том, как мы разрабатываем front-end в Тинькофф Банке, чтобы вы знали об инструментах, которые облегчают нам жизнь.
Этапы процесса разработки
До того как задача попадает разработчику, она проходит стадию спецификации. На выходе в идеальном варианте получается задача в JIRA + описание в WIKI + готовые дизайны. После этого задача поступает разработчику, а когда разработка закончена, задачу передают в отдел тестирования. Если оно пройдет успешно, релиз выходит в паблик.
В работе мы используем следующие инструменты (их выбор, в том числе, обоснован упрощением процесса разработки и взаимодействия с менеджерами):
Все продукты Atlassian отлично интегрируются друг с другом и с TeamCity.
В качестве Git Branch Workflow мы решили использовать привычный Feature Branch Workflow, подробнее о котором можно прочитать здесь [1].
В нескольких словах, все сводится к следующему:
Atlassian Stash позволяет в пару кликов настроить подобный Workflow и комфортно работать с ним, позволяя:
Также очень удобно настраивается интеграция Atlassian Stash с TeamCity. Мы настроили ее так, что при создании нового pull request или внесении изменений в уже имеющийся, TeamCity автоматически запускает сборку и тестирование кода для этого pull request, а в Stash мы выставили настройку запрета merge до тех пор, пока билд и тесты не завершатся успешно. Это позволяет нам держать код в родительских ветках в работоспособном состоянии.
Front-end-тестирование в Тинькофф Банке охватывает только критически важные участки кода: бизнес логику, расчеты и общие компоненты. Визуальную часть UI тестирует наш отдел QA. При написании тестов мы руководствуемся следующими принципами:
Если один из этих принципов не выполняется, то код необходимо доработать, чтобы его было легче тестировать.
Лучше всего, если компоненты слабо связаны между собой, но так получается не всегда. В этом случае мы используем метод декомпозиции:
Так как мы тестируем поведение, описывая идеальную работу кода, необходимо разработать эталон поведения кода, а также предусмотреть возможные ситуации, при которых код будет ломаться. То есть тест должен описывать правильное поведение кода и реагировать на ошибочные ситуации. Такой подход позволяет сформировать на выходе спецификацию кода и при рефакторинге исключить риск поломки.
При таком подходе разработка сводится к трем шагам:
Чтобы писать тесты, необходимо выбрать test runner и test framework. В нашем процессе разработки используется следующий стек технологий:
Мы запускаем тесты как локально, так и в CI (TeamCity). В CI тесты запускаются в PhantomJS, а отчеты генерируются с помощью teamcity-karma-reporter.
Итак, приступим к практике. Я уже сделал небольшую заготовку проекта, код которого можно найти тут [2]. Что с этим делать, думаю, всем должно быть понятно.
Не буду описывать, как настраивать Karma и Gulp, все описано в официальной документации на сайтах проектов.
Мы будем запускать Karma в связке с Gulp. Напишем два простых таска — для запуска тестов и watch для слежки за изменениями с автозапуском тестов.
В Jasmine есть практически все, что может потребоваться для тестирования UI: matchers, spies, setUp / tearDown, stubs, timers.
Остановимся чуть подробнее на matchers:
toBe — равно
toEqual — тождество
toMatch — регулярное выражение
toBeDefined / toBeUndefined — проверка на существование
toBeNull — null
toBeTruthy / toBeFalse — истина или ложь
toContain — наличие подстроки в строке
toBeLessThan / toBeGreaterThan — сравнение
toBeCloseTo — сравнение дробных значений
toThrow — перехват исключений
Каждый из matchers может сопровождаться исключением not, например:
expect(false).not.toBeTruthy()
Рассмотрим простой пример: допустим, необходимо реализовать функцию, которая возвращает сумму двух чисел.
Первое, что надо сделать — написать тест:
describe('Matchers spec', function() {
it("should return sum of 2 and 3", function() {
expect(sum(2, 3)).toEqual(5);
});
})
Теперь сделаем так, чтобы тест был пройден:
function sum(a, b) {
return a + b;
}
Теперь пример немного сложнее: напишем функцию расчета площади круга. Как в прошлый раз, пишем тест, а потом код.
describe('Matchers spec', function() {
it("should return area of circle with radius 5", function() {
expect(circleArea(5)).toBeCloseTo(78.5, 1);
});
})
function circleArea(r) {
return Math.PI * r * r;
}
Так как у нас есть тесты, то можно, не боясь провести рефакторинг кода, использовать функцию Math.pow:
function circleArea(r) {
return Math.PI * Math.pow(r, 2);
}
Тесты снова пройдены — код работает.
Matchers довольно просты в использовании, и подробнее останавливаться на них нет смысла. Перейдем к более продвинутому функционалу.
В большинстве ситуаций нужно тестировать функционал, который требует предварительной инициализации, например, переменных окружения, а также позволяет избавиться от дублирования кода в спеках. Чтобы при каждом Spec не проводить эту инициализацию, в Jasmine предусмотрены setUp и tearDown.
beforeEach — выполнение действий, необходимых для каждого Spec
afterEach — выполнение действий после каждого Spec
beforeAll — выполнение действий перед запуском всех Specs
afterAll — выполнение действий после выполнения всех Specs
При этом совместное использование ресурсов между каждыми тест-кейсами можно выполнять двумя способами:
Чтобы лучше понять, как можно использовать setUp и tearDown, сразу приведу пример с использованием Spies.
describe('Learn Spies, setUp and tearDown', function() {
beforeEach(function(){
this.testObj = {//используем this для шаринга ресурсов
myfunc: function(x) {
someValue = x;
}
}
spyOn(this.testObj, 'myfunc');//создаем Spies
});
it('should call myfunc', function(){
this.testObj.myfunc('test');//вызываем функцию
expect(this.testObj.myfunc).toHaveBeenCalled();//проверяем, что myfunc вызывался
});
it('should call myfunc with value 'Hello'', function(){
this.testObj.myfunc('Hello');
expect(this.testObj.myfunc).toHaveBeenCalledWith('Hello');//проверяем, что myfunc вызывался с Hello
});
});
spyOn, по существу, создает обертку над нашим методом, которая вызывает исходный метод и сохраняет аргументы вызова и флаг вызова метода.
Это не все возможности Spies. Подробнее можно прочитать в официальной документации.
Javascript — асинхронный язык, поэтому сложно представить код, который необходимо тестировать без асинхронных вызовов. Весь смысл сводится к следующему:
describe('Try async Specs', function() {
var val = 0;
it('should call async', function(done) {
setTimeout(function(){
val++;
done();
}, 1000);
});
it('val should equeal to 1', function(){
expect(val).toEqual(1);//вызовется только после выполнения done, либо по окончанию DEFAULT_TIMEOUT_INTERVAL
});
});
SinonJS мы используем в основном для тестирования функционала, который делает AJAX- запросы к API. В SinonJS для тестирования AJAX есть несколько способов:
Мы используем более гибкий подход fakeServer, который позволяет отвечать на AJAX-запросы подготовленными заранее JSON mocks. Так логику работы с API можно тестировать более детально.
describe('Use SinonJS fakeServer', function() {
var fakeServer, spy, response = JSON.stringify({ "status" : "success"});
beforeEach(function(){
fakeServer = sinon.fakeServer.create();//создаем fake server
});
afterEach(function(){
fakeServer.restore();//сбрасываем fake server
});
it('should call AJAX request', function(done){
var request = new XMLHttpRequest();
spy = jasmine.createSpy('spy');//создаем Spies
request.open('GET', 'https://some-fake-server.com/', true);
request.onreadystatechange = function() {
if(request.readyState == 4 && request.status == 200) {
spy(request.responseText);//запрос выполнен
done();
}
};
request.send(null);
//отвечаем на первый запрос
fakeServer.requests[0].respond(
200,
{ "Content-Type": "application/json" },
response
);
});
it('should respond with JSON', function(){
expect(spy).toHaveBeenCalledWith(response);//проверяем ответ
});
});
В данном примере использовался самый простой способ ответа на запросы, но SinonJS позволяет создавать и более гибкие настройки fakeServer с укзанием мапы url, method и ответа, то есть предоставляет возможность полностью сэмулировать работу API.
Писать тесты круто и увлекательно. Не стоит думать, что при таком подходе разработка усложняется и растягивается по срокам.
У тестирования кода есть ряд преимуществ:
Самое главное: помнить, что тесты — это тот же самый код, а следовательно, надо быть предельно внимательным при их написании. Некорректно работающий тест не сможет сигнализировать об ошибке в коде.
Автор: ekubyshin
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/83957
Ссылки в тексте:
[1] здесь: https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow/
[2] тут: https://github.com/ekubyshin/how-to-write-test
[3] JasmineBDD: http://jasmine.github.io/
[4] SinonJS: http://sinonjs.org/
[5] Karma: http://karma-runner.github.io/0.12/index.html
[6] Книга Testable Javascript: http://www.amazon.com/Testable-JavaScript-Mark-Ethan-Trostler/dp/1449323391/
[7] Книга Test-Driven Javascript Development: http://www.amazon.com/Test-Driven-JavaScript-Development-Developers-Library/dp/0321683919/
[8] Источник: http://habrahabr.ru/post/251421/
Нажмите здесь для печати.