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

в 11:40, , рубрики: bdd, git workflow, gulp, jasmine, javascript, karma, tdd, Блог компании Тинькофф Банк, Веб-разработка, интерфейсы, тестирование веб-приложений, тинькофф банк, метки:

В продолжение предыдущей статьи о тестировании интерфейсов в Тинькофф Банке расскажу, как мы пишем 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 мы решили использовать привычный Feature Branch 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. рефакторим код.

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

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

Чтобы писать тесты, необходимо выбрать 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. Feature Branch Workflow;
  7. Код.

Автор: ekubyshin

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js