Тестируй это!
Виктор Русакович
GP Solutions
Минск, Беларусь
Любите тесты?
Любите писать тесты?
Любите запускать тесты?
Любите вручную запускать тесты?
Любите ждать тесты?
Менеджер не понимает, зачем вам тесты?
Как я вас понимаю...
2
3
4
5
Наше старое приложение
Back-end (API) - Java 90%, Scala - 9%
Front-end - JavaScript 90%, SCSS - 10%
Javascript - jQuery, RxJS
6
Наше старое приложение
Javascript “features” - 55725 строк кода
Javascript “tests” - 41516 строк кода
7
Back-end: 1000 юнит тестов, 600 rest тестов
Front-end: 6500 8500 юнит тестов
Selenium: 150 тестов
Тесты старого приложения
8
Последовательно Параллельно
BE - Java UNIT 30c 30c :)
BE - REST API 3600c 600c
FE - Mocha (локально) 900c 300c
FE - Mocha (сервер) 1800c 500c
Selenium 3600c 600c
9
10
Ускоряем тесты (не удаляя тесты)
параллельный подход
компонентный подход
11
12
Последовательно Параллельно
BE - Java UNIT 30c 30c :)
BE - REST API 3600c 600c
FE - Mocha (локально) 900c 300c
FE - Mocha (сервер) 1800c 500c
Selenium 3600c 600c
13
before((done) => {
booking.loadApplication(done, {day: 1})
})
14
Рендерим всё приложение
before(() => {
booking.loadApplication({ selectedDate: today })
.then(() => {
calendarEl('.date').trigger('click')
})
})
15
1мин - 200 тестов 16
17
Рендерим только компонент, а не всё (helper)
function makeComponent() {
$el = Helpers.componentTestContainer('<section></section>')
var calendar = CalendarView({root: $el})
$el.find('.calendar).show()
calendar.render()
}
18
Проверяем компонент, а не всё (before())
before(() => {
booking.loadApplication({})
makeComponent()
$calendarEl('.date').trigger('click')
})
19
20
1мин - 3000 тестов
21
22
== +
23
Тесты для готового приложения
(не React и не Angular)
запускать проверять подменять
24
Sinon.js
1. Spies
2. Stubs
3. Mocks (stub + assertion)
4. Fake timers
5. Fake XHR and server
25
Шпионы Зинона
describe('Component', () => {
const clickSpy = sinon.spy()
before(() => {
renderComponent({ onClick: clickSpy })
TestUtils.Simulate.click(el.querySelector('button'))
})
it('calls function with "evening" as argument', () => {
clickSpy.should.be.calledOnce
clickSpy.should.be.calledWith('evening')
})
})
26
Зинон меняет время
describe('Component', () => {
const clock = sinon.useFakeTimers('setTimeout')
before(() => {
renderComponent()
clock.tick(3000)
})
it('hides element after timeout', () => {
el.should.exist })
})
27
Уберите за собой!!!
after(() => {
clock.restore()
})
28
Шаг 1. Добавляем библиотеки
<link rel="stylesheet" media="all" href="mocha.css">
<script src="mocha.js"></script>
<script src="chai.js"></script>
29
Шаг 2. Настраиваем Mocha и Chai
<script>
mocha.setup('bdd|tdd')
chai.should()
</script>
…
<div id="mocha"></div>
30
BDD TDD
describe(‘Component`, () => {
before()
beforeEach()
after()
afterEach()
it(‘works’, () => {
el.should.exist
})
})
suite(‘Component’, () => {
suiteSetup()
setup()
suiteTeardown()
teardown()
test(‘works’, () => {
el.should.exist
})
})
31
mocha.js
context.setup = common.beforeEach;
context.teardown = common.afterEach;
context.suiteSetup = common.before;
context.suiteTeardown = common.after;
https://github.com/mochajs/mocha/blob/master/mocha.js#L1191
32
Шаг 3. Загружаем тесты и запускаем их
<script src="App.js"></script>
...
<script src="App.spec.js"></script>
<script>
$(document).ready(mocha.run)
</script>
33
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="app.css">
<script src="App.js"></script>
</head>
<body>
<h1>Instant result</h1>
<div id="mocha"></div>
<link rel="stylesheet" media="all" href="https://cdnjs….mocha/3.4.2/mocha.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.4.2/mocha.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.0.0/chai.js"></script>
<script>
mocha.setup('bdd')
chai.should()
</script>
<script src="App.spec.js"></script>
<script>
$(document).ready(mocha.run)
</script>
</body>
</html>
34
35
Шаг 4. Открываем в браузере /indexTest.html
36
Шаг 5. Статус
37
Шаг 6. Ошибка
38
39
C:>npm run test
40
41
1800 тестов за 60 сек
или
8500 тестов за 900 сек
42
43
44
45
46
47
48
49
Тесты для компонентного подхода
(React)
запускать проверять подменять
50
<Warning
country="RU"
mobile={false}
error={false}/>
51
Что проверяем?
1. Видимость (отрендерился или спрятался)
2. Классы у DOM элелементов
3. “Переводы” - текст внутри элементов
4. setTimeout / setInterval
5. Всё остальное
52
Render helper - создаём
function renderWarning(newParams) {
const defaultParams = {
mobile: false,
country: 'XZ',
error: false
}
const params = { ...defaultParams, ...newParams }
const comp = TestUtils.renderIntoDocument(<Warning {...params} />)
return ReactDOM.findDOMNode(comp)
}
53
Render helper - используем
testEl_1 = renderWarning({ mobile: true })
testEl_1 = <Warning country="XZ" mobile={true} error={false}/>
testEl_2 = renderWarning({ country: "EN", error: true })
testEl_2 = <Warning country="EN" mobile={false} error={true}/>
const defaultParams = {
mobile: false,
country: 'XZ',
error: false
}
54
Помощники для DOM тестов - chai-dom
1) document.getElementById('bar')
.should.have.class('foo')
2) document.querySelector('.name')
.should.have.text('John Doe')
vs
1) document.getElementById('bar')
.classList.indexOf('foo').should.be.gt(-1)
2) document.querySelector('.name')
.innerText.should.contain('John Doe')
55
И даже QR?
56
Тестируем QR код
it('shows QR (Aztec) code width as 196px', () => {
qr.should.have.property('width', 196)
})
it('shows black pixel in the middle of QR (Aztec) code', () => {
const blackPxSeq = [0, 0, 0, 255].join(',')
const middlePoint = Math.floor(canvasContext.canvas.width /2)
canvasContext
.getImageData(middlePoint, middlePoint, 1, 1)
.data.join(',')
.should.eql(blackPxSeq)
})
57
Как писать тесты? Рассказываем историю
describe('when rendering mobile with error with RU country', () => {
before(() => {
el = renderWarning({ mobile: true, error: true, country: 'RU' })
})
it('adds "mobile" class', () => {
el.should.have.class('mobile')
})
it('shows "call support" button', () => {
el.querySelector('.bi-button-call-support').should.exist
})
})
58
класс?
отрисовался?
describe.only / it.only
describe.only('when rendering mobile with error with RU country', () => {
before(() => {
el = renderWarning({ mobile: true, error: true, country: 'RU' })
})
it('adds "mobile" class', () => {
el.should.have.class('mobile')
})
it('shows "call support" button', () => {
el.querySelector('.bi-button-call-support').should.exist
})
})
59
60
when rendering mobile with error with RU country
✓ adds "mobile" class
✓ shows "call support" button
Passed 2 tests of 2
61
import ...
describe('Component', () => {
let el
before(() => {
el = renderComponent({ prop: 'value' })
})
it('does something', () => {
el.should.exist
})
})
function renderComponent(params = {}) {...}
62
Храним тесты
63
Разрабатываем и наблюдаем
64
Разрабатываем и наблюдаем
> view karma.conf.js
module.exports = function(config) {
config.set({
browsers: ['Chrome'],
plugins: [
'karma-notify-reporter'
],
notifyReporter: { reportEachFailure: true, reportSuccess: true }
> npm run test -- --reporters dots,notify
65
Continuous Integration
66
Как подготовиться к CI?
● запуск тестов по команде, “npm test”
● скрипт запуска завершается после окончания тестов:
○ код выхода == 0 - тесты прошли успешно
○ код выхода != 0 - тесты упали
● создается отчет (по желанию) для построения списка тестов:
junit.xml, TAP
67
Тесты в облаке - Travis-CI/Snap-CI
68
Форматы отчетов - JUnit (xml)
<testsuite tests="2">
<testcase classname="file.js" name="Input file opened"/>
<testcase classname="file.js" name="First line is empty">
<failure type="error">Expected “XAXA” to equal “”</failure>
</testcase>
</testsuite>
69
Форматы отчетов - TAP (Test Anything Protocol)
1..2
ok 1 - Input file opened
not ok 2 - First line of the input valid
70
Консольный браузер
npm install mocha-phantomjs
71
"dependencies": {
"mocha-phantomjs": "3.3.2"
},
"scripts" : {
"test": "mocha-phantomjs index.html"
}
Единая точка входа в тесты и приложение
<script>
if (location.href.indexOf('runTest') !== -1)
mocha.run()
if (window.mochaPhantomJS)
mochaPhantomJS.run()
</script>
72
Travis CI
● Облачный сервис для запуска тестов
● Легкая интеграция с github
○ бесплатно для открытых проектов
○ от $120 в месяц для закрытых (100 первых сборок - бесплатно!)
● JavaScript, PHP, C++, Ruby, Visual Basic
73
Snap-CI
● Облачный сервис для запуска тестов
● Простая интеграция - привязать репозиторий и ввести команду для
тестов
○ Бесплатно для открытых проектов
○ от $30 для закрытых проектов
● Больше настроек через GUI, например, запуск по расписанию (cron) -
зачем?
● Дешевле
● Менее популярен среди open-source проектов
August 1st 2017
74
Демо
75
76
← dashboard
Github - readme.md ↓
77
78
79
80
ГДЕ?!
Привлекаем внимание
81
КТО?!
82
83
84
85
86
87
Ethernet shield
88
Arduiono
Arduino 4-channel Relay
89
90
Числа
91
100%Столько тестов должны “проходить” перед релизом
92
0%Столько тестов можно “сломать” в пятницу вечером
93
94
сложность
рефакторинга
Спасибо!
Вопросы?
Виктор Русакович из Минска
GP Solutions

Тестируй это / Виктор Русакович (GP Solutions)