Підвищуємо стабільність Front-end

В продовження попередньої статті про тестування інтерфейсів в Тінькофф Банку розповім, як ми пишемо 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. рефакторимо код.


Інструментарій розробника

Щоб писати тести, необхідно вибрати 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® {
return Math.PI * r * r;
}


Так як у нас є тести, то можна, не боячись провести рефакторинг коду, використовувати функцію Math.pow:
function circleArea® {
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. Facebook;
  4. Книга Testable Javascript;
  5. Книга Test-Driven Javascript Development;
  6. Feature Branch Workflow;
  7. Код.

Який testing framework використовуєте ви

/>
/>


<input type=«radio» id=«vv65443»
class=«radio js-field-data»
name=«variant[]»
value=«65443» />
Jasmine
<input type=«radio» id=«vv65445»
class=«radio js-field-data»
name=«variant[]»
value=«65445» />
Mocha
<input type=«radio» id=«vv65447»
class=«radio js-field-data»
name=«variant[]»
value=«65447» />
QUnit
<input type=«radio» id=«vv65449»
class=«radio js-field-data»
name=«variant[]»
value=«65449» />
Buster.JS
<input type=«radio» id=«vv65451»
class=«radio js-field-data»
name=«variant[]»
value=«65451» />
Інший

Проголосувало 6 осіб. Утрималося 7 осіб.


Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.