company_banner

Повышаем стабильность Front-end

  • Tutorial
В продолжение предыдущей статьи о тестировании интерфейсов в Тинькофф Банке расскажу, как мы пишем unit-тесты на javascript.

image


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

Несколько слов о разработке


Сначала о том, как мы разрабатываем front-end в Тинькофф Банке, чтобы вы знали об инструментах, которые облегчают нам жизнь.

Этапы процесса разработки

  1. Постановка задачи
  2. Написание технического задания
  3. Разработка дизайнов
  4. Разработка кода и unit-тестов
  5. Тестирование отделом QA и отладка
  6. Запуск в боевом окружении

До того как задача попадает разработчику, она проходит стадию спецификации. На выходе в идеальном варианте получается задача в JIRA + описание в WIKI + готовые дизайны. После этого задача поступает разработчику, а когда разработка закончена, задачу передают в отдел тестирования. Если оно пройдет успешно, релиз выходит в паблик.

В работе мы используем следующие инструменты (их выбор, в том числе, обоснован упрощением процесса разработки и взаимодействия с менеджерами):
  1. Atlassian Jira;
  2. Atlassian Stash;
  3. Atlassian Confluence;
  4. JetBrains TeamCity;
  5. JetBrains IntelliJ Idea.

Все продукты Atlassian отлично интегрируются друг с другом и с TeamCity.

В качестве Git Branch Workflow мы решили использовать привычный Gitflow Workflow, подробнее о котором можно прочитать здесь.

В нескольких словах, все сводится к следующему:
  1. есть две основных ветки master, что соответствует последнему релизу, и develop, где содержатся все последние изменения;
  2. для каждого релиза от develop-ветки создается релизная ветка, например, release-1.0.0;
  3. дальнейшие правки по релизу мерджатся в релизную ветку;
  4. после успешного релиза release-1.0.0 мерджится в master-ветку и может быть удалена.

Atlassian Stash позволяет в пару кликов настроить подобный Workflow и комфортно работать с ним, позволяя:
  1. проверять наименования веток;
  2. запрещать merge напрямую в родительские ветки;
  3. автоматически мерджить pull requests из release-ветки в develop-ветку, а при возникновении конфликтов автоматом создавать ветку для разрешения конфликта;
  4. запрещать мерджить pull request, если задача в jira находится в некорректном статусе, например, в «In Progress» вместо «Ready».

Также очень удобно настраивается интеграция Atlassian Stash с TeamCity. Мы настроили ее так, что при создании нового pull request или внесении изменений в уже имеющийся, TeamCity автоматически запускает сборку и тестирование кода для этого pull request, а в Stash мы выставили настройку запрета merge до тех пор, пока билд и тесты не завершатся успешно. Это позволяет нам держать код в родительских ветках в работоспособном состоянии.

Немного теории


Front-end-тестирование в Тинькофф Банке охватывает только критически важные участки кода: бизнес логику, расчеты и общие компоненты. Визуальную часть UI тестирует наш отдел QA. При написании тестов мы руководствуемся следующими принципами:
  1. код должен быть модульным, а не монолитным, так как тесты пишутся для данного юнита;
  2. слабая связанность между компонентами;
  3. каждый юнит должен решать одну задачу, а не быть универсальным.

Если один из этих принципов не выполняется, то код необходимо доработать, чтобы его было легче тестировать.

Лучше всего, если компоненты слабо связаны между собой, но так получается не всегда. В этом случае мы используем метод декомпозиции:
  1. тестируем каждый компонент в отдельности и убеждаемся, что тесты проходят, а компоненты работают корректно;
  2. тестируем зависимый компонент обособленно от других модулей, используя Mocks.

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

При таком подходе разработка сводится к трем шагам:
  1. пишем тест и смотрим, как он фейлится;
  2. пишем код, чтобы тест был успешно пройден;
  3. рефакторим код.



Инструментарий разработчика


Чтобы писать тесты, необходимо выбрать test runner и test framework. В нашем процессе разработки используется следующий стек технологий:
  1. Jasmine BDD Testing framework;
  2. SinonJS;
  3. Karma;
  4. PhantomJS или любой другой браузер;
  5. NodeJS;
  6. Gulp.

Мы запускаем тесты как локально, так и в CI (TeamCity). В CI тесты запускаются в PhantomJS, а отчеты генерируются с помощью teamcity-karma-reporter.

Практика


Итак, приступим к практике. Я уже сделал небольшую заготовку проекта, код которого можно найти тут. Что с этим делать, думаю, всем должно быть понятно.

Не буду описывать, как настраивать Karma и Gulp, все описано в официальной документации на сайтах проектов.

Мы будем запускать Karma в связке с Gulp. Напишем два простых таска — для запуска тестов и watch для слежки за изменениями с автозапуском тестов.

JasmineBDD

В 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

При этом совместное использование ресурсов между каждыми тест-кейсами можно выполнять двумя способами:
  1. использовать локальную переменную для тест-кейса (код);
  2. использовать this;

Чтобы лучше понять, как можно использовать 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 — асинхронный язык, поэтому сложно представить код, который необходимо тестировать без асинхронных вызовов. Весь смысл сводится к следующему:
  1. beforeEach, it и afterEach принимают опциональный callback, который необходимо вызывать после выполнения асинхронного вызова;
  2. Specs не будет выполнен, пока callback не запустится, либо пока не закончится DEFAULT_TIMEOUT_INTERVAL

Код
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

SinonJS мы используем в основном для тестирования функционала, который делает AJAX- запросы к API. В SinonJS для тестирования AJAX есть несколько способов:
  1. создать stub на функцию AJAX-вызова, используя sinon.stub;
  2. использовать fake XMLHttpRequest, который подменяет нативный XMLHTTPRequest на фейковый;
  3. создать более гибкий fakeServer, который будет отвечать на все 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.

P.S.


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

У тестирования кода есть ряд преимуществ:
  1. код, покрытый тестами, можно рефакторить без страха поломать его;
  2. на выходе предоставляется спецификация кода, выраженная тестами;
  3. разработка идет быстрее, так как нет необходимости вручную проверять работоспособность кода — для этого уже написаны тесты и контрольные примеры.

Самое главное: помнить, что тесты — это тот же самый код, а следовательно, надо быть предельно внимательным при их написании. Некорректно работающий тест не сможет сигнализировать об ошибке в коде.

Ресурсы


  1. JasmineBDD;
  2. SinonJS;
  3. Karma;
  4. Книга Testable Javascript;
  5. Книга Test-Driven Javascript Development;
  6. Gitflow Workflow;
  7. Код.

Only registered users can participate in poll. Log in, please.

Какой testing framework используете вы

  • 37.3%Jasmine93
  • 38.5%Mocha96
  • 10%QUnit25
  • 0.4%Buster.JS1
  • 13.6%Другой34
Tinkoff.ru
439.01
IT’s Tinkoff.ru — просто о сложном
Share post

Comments 35

    –8
    Когда ваша команда еще немного подрастет, вам придется слезть с JIRA и других инструментов от Atlassian по причине их низкой производительности.
      0
      Предложения?
        +1
        Redmine по моему наилучший вариант, поставил на свой сервак и не парся.
          0
          Jira + Greenhopper = свой сервак + скорость + интеграция с всем уже перечисленным в статье.
          +5
          Да какие там предложения, автор комментария получил свои минуса и скрылся)
          +5
          Каково количество юзеров, с которых начинаются тормоза?
            +1
            плюсую, тоже очень интересно что не так там с производительностью.
              +1
              У нас больше сотни юзеров. Тормозов системы не наблюдаю.
                –3
                Речь идет о тысячах пользователей.
                  0
                  Так на тысячи пользователей — это уже Atlassian Enterprise и его «Data Center» опции, с нужной масштабируемостью. Что, там тоже «проблемы с производительностью»?
                    +1
                    Яндекс в 2013-м (используя тогда 5.5.х) писал, что таки да и что пилит свое решение. Чем всё закончилось не отслеживал.
                      0
                      Надо попросить Яндекс поделиться сведениями в их блоге, было бы интересно:)

                    +1
                    На понимание: Jira у нас сейчас используется не только в подразделениях, занимающихся разработкой. Так что упомянутые тысячи пользователей в наличии и, как верно заметил коллега, «все шевелится».

                    Хотя, должен заметить, такое количество пользователей заставляет очень осторожно подходить к выбору плагинов. Неоднократно сталкивались с существенной деградацией производительности после установки некоторых из них.
                +4
                ну пока шевелится вроде
                  +2
                  Устанавливаем JIRA Server Edition сейчас. Было бы здорово услышать что нетак с системой.
                    0
                    На 100 пользователей достаточно простой виртуальной машины с 4Gb памяти и парой процессорных ядер.
                    +2
                    «Команда немного подрастет» и «Речь идет о тысячах пользователей» — Ваши два комментария как-то не вяжутся между собой.
                    +1
                    А зачем вы используете Sinon, если у вас проект на Angular и там есть встроенный FakeServer, называемый $httpBackend?
                      +1
                      У нас есть проекты, которые не на AngularJS, там мы используем Sinon. Мне не хотелось тащить Angular в качестве примера, поэтому решил показать на примере с Sinon.

                      А в ИБ мы используем angular-mocks как ты и написал.
                        +1
                        Хорошо, значит вы знаете как удобно, когда angular-mocks бросает ошибку, если произошел запрос, для которого не написан mock.
                        Sinon в этом случае его пропускает, а это не есть хорошо. Решали как-нибудь эту проблему?
                          +1
                          В этом проблемы особой нет, имхо, т.к. в любом случае тестируется поведение юнита, который должен не просто бросить запрос, но и получить данные, поэтому более правильно будет все таки проверять, чтобы модуль бросал исключение и перехватывать это в самом тесте.

                          Кроме того, так как тестируется async-код, то в любом случае надо проверять вызвался ли callback, он либо вызовется, либо упадет по DEFAULT_TIMEOUT_INTERVAL.

                          Какие с этим проблемы возникали?

                            +1
                            Такой пример: есть сервис по работе с сессией пользователя. Иногда ему нужно сделать запрос на сервер, чтобы узнать нужную информацию, а иногда он может достать её из cookies. Как в таком случае проверить, что, если информация приходит из cookies, то запроса на сервер не случалось?

                            Лучше быть уверенным, что совершаются только нужные запросы, чтобы не страдала производительность.
                              +1
                              Смотри, смысл в том, что надо как можно тщательнее сделать декомпозицию юнита, чтобы он выполнял что-то одно, а не два действия, как в твоем примере. Те этот сервис вполне можно декомпозировать, допустим на, getFromCookies и getFromAPI. В таком случае, будет немного проще решить эту задачу, проверив, что будет вызываться.

                              Допустим возьмем Backbone, там все происходит на моделях, те воспользуемся Mock и протестируем работоспособность модели, триггерятся ли на ней события или нет, вызываются ли Spies или еще как то.

                              Те вполне можно оперирую возможностями Jasmine добиться нужного поведения, если модуль сложно покрыть тестами и отловить его исключения, то надо посмотреть на код, возможно, надо что-то детальнее декомпозировать и разнести по отдельным юнитам и тд.

                              Вообще по изучаю эту тему на досуге.

                              Спасибо за вопрос.
                                0
                                И снова здравствуйте.
                                Мне пришлось снова разбираться в вопросе тестов и я нашел еще одно решение для mock-ajax. Может и вас заинтересует:

                                jasmine.github.io/2.2/ajax.html

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

                                  0
                                  крутяк. спасибо.
                      0
                      А как Вам TeamCity? Все ли устраивает? Пользовались им изначально, или перешли с какого-то другого продукта (CI)?
                        0
                        Года 3 точно его используем и в принципе все устраивает, есть правда небольшие глюки, в основном он бывает обманывает с зависимостями сборки, а так в целом работает без нареканий.
                        0
                        А зачем используется Sinon и Jasmine, если в последнем уже есть mock/stub?
                          0
                          из sinon используем fake server. mock/stub другое совсем
                          0
                          Mocha
                            0
                            Вы работаете с GitFlow, а не Feature Branch. Это очевидно из приведенной вами же ссылки.
                              0
                              точно!

                              спасибо.
                              0
                              баг-репорт

                              image
                                0
                                На какой email посылать баги по фронту?
                                  0
                                  internetbank@tinkoff.ru

                                Only users with full accounts can post comments. Log in, please.