SlideShare a Scribd company logo
Юнит-тестирование скриншотами:
преодолеваем звуковой барьер
Роман Дворнов
Avito
Москва, 2017
Работаю в Avito
Open source:

basis.js, CSSO, 

component-inspector, 

csstree, rempl и другие
Доклад-история
про поиски инженерных решений
3
Вкратце
• Делать велосипеды не всегда плохо
• Если не сдаваться – решение найдется
• Как полезно разобраться в графических форматах
• Ускорять можно не только оптимизируя код
• Экспериментируя можно получить неожиданные
результаты (интрига)
4
5
В нас пропал дух авантюризма
Тестирование скриншотами
Говорим про решение 

для юнит-тестов
компонент/блоков
Тестирование бывает разным
• Юнит-тесты
• Функциональное
• Интеграционное
• ...
7
Основные требования
• Просто
• Быстро
• Дешево
8
Возможные решения
• Использовать готовые сервисы
• Использовать готовые инструменты (Gemini)
• Написать свое 🤗
9
Сервисы нам не подходят,

на то "есть причины"
10
Готовые инструменты
11
Обычно ориентированы на посещение
урлов и снятие скриншотов
12
Инструменты: Gemini
13
Вероятно крутая штука, но похоже на
космический корабль...
Мой быстрый тест
(проверка 100 изображений 282х200):
1 мин 57 сек
Стали делать своё 🤗
14
15
test('button', async () => {
const snap = snapshot(<Button>Button</Button>);
expect(snap.snapshot).toMatchSnapshot();
expect(await snap.screenshot).toMatchSnapshotImage();
});
Свое решение: просто
Свое решение: быстро
• Сравнение скришотов (800x600)
• ~0ms/скриншот, если совпадают
• ~100ms/скриншот, если есть разница
• Обновление 25ms/скриншот (800x600)
16
Делаем сами: план
• Сгенерировать статичную разметку с
необходимыми стилями и ресурсами
• Загрузить разметку в браузер и сделать
скриншот
• Сравнить скриншот с эталонным
изображением
17
Генерация разметки
Генерация разметки: план
• Генерируем HTML компонента
• Определяем зависимости
• Находим стили, обрабатываем, включаем
• Находим изображения, обрабатываем, инлайним
• Получаем HTML документа без локальных
зависимостей
19
Генерация HTML
В нашем случае – React
21
⚠
Способ зависит от стека
Генерация HTML компонента: React
• react-dom/server + jsdom
22
const ReactDOMServer = require('react-dom/server');
const html = ReactDOMServer.renderToStaticMarkup(jsx);
Пока все просто 😉
23
Генерация CSS
Генерация CSS: план
• Найти CSS файлы
• Преобразовать
• Заменить локальные ссылки на инлайн
• Избавиться от динамических частей
• Склеить
25
Jest / Babel / CSS Modules
26
⚠
Способ зависит от стека
Поиск CSS
Поиск CSS файлов: CSS Modules
• В CSS Modules CSS подключается как
обычный модуль:

import 'path/to/style.css';

require('path/to/style.css');
• Нужно перехватить все вызовы 

import/require() для CSS и сохранить пути
28
Напишем свой плагин 😉
29
30
"jest": {
...
"transform": {
".js$": "./scripts/jest-transform.js"
},
...
}
Подключаем плагин: в настройках Jest
31
const { createTransformer } = require('babel-jest');
module.exports = createTransformer({
...
plugins: [
...
'path/to/collect-styles-path.js',
...
]
});
Подключаем плагин: jest-transform.js
Поиск CSS: Пишем плагин для Babel
• import '*.css' → require('*.css')
• require('*.css') →
32
(function(){
global.includedCssModules = global.includedCssModules || [];
if (includedCssModules.indexOf('path/to/style.css') === -1) {
includedCssModules.push('path/to/style.css');
}
return { ... original exports ... };
})()
Код плагина полностью (52 SLOC)
На момент генерации разметки компонента 

в includedCssModules будут пути 

всех подключенных файлов стилей
33
Обработка CSS
Обработка CSS: план
• Инлайн ресурсов
• Выключаем динамику
35
Обработка CSS: инлайн ресурсов
Напишем свой плагин 😉
37
38
const { createTransformer } = require('babel-jest');
module.exports = createTransformer({
...
plugins: [
...
['css-modules-transform', {
processCss: 'path/to/process-css.js',
...
}],
...
]
});
Подключаем плагин: jest-transform.js
CSSTree
• Быстрый
• Детальный
• Толерантен к ошибкам
39
github.com/csstree/csstree
CSS парсер (и не только)
Инлайн ресурсов: трансформация CSS
40
const path = require('path');
const csstree = require('css-tree');
module.exports = function(data, filename) {
const ast = csstree.parse(data);
csstree.walk(ast, function(node) {
if (node.type === 'Url') {
const { type, value } = node.value;
const url = type === 'String' ? value.substring(1, value.length - 1) : value;
node.value = {
type: 'Raw',
value: inlineResource(url, path.dirname(filename))
};
}
});
return csstree.translate(ast);
}
«наивная» реализация
TODO: @imports & edge cases
Инлайн ресурсов: url → dataURI
41
const fs = require('fs');
const mime = require('mime');
function inlineResource(uri, baseURI) {
const filepath = path.resolve(baseURI, uri);
const data = fs.readFileSync(filepath);
const mimeType = mime.getType(filepath);
return 'data:' + mimeType + ';base64,' + data.toString('base64');
}
Инлайн ресурсов
в 26 SLOC*
42
* Source Lines Of Code
Обработка CSS: 

избавляемся от динамики
Проблема:
Динамика (например, анимация)
приводит к разным скриншотам – 

ее необходимо «выключить»
44
Источник динамики
• CSS Transitions
• CSS Animations
• Каретка в полях ввода
• GIF
• ...
45
46
*, *::after, *::before {
transition-delay: 0s !important;
transition-duration: 0s !important;
}
Выключаем: CSS Transitions
Переводит анимацию 

в финальное состояние
47
*, *::after, *::before {
animation-delay: -0.0001s !important;
animation-duration: 0s !important;
animation-play-state: paused !important;
}
Выключаем: CSS Animations
Переводит анимацию 

в финальное состояние
48
*, *::after, *::before {
animation-delay: -0.0001s !important;
animation-duration: 0s !important;
animation-play-state: paused !important;
}
Выключаем: CSS Animations
Иначе не работает в Safari 🤗
49
*, *::after, *::before {
animation-delay: -0.0001s !important;
animation-duration: 0s !important;
animation-play-state: paused !important;
}
Выключаем: CSS Animations
Не дает анимации проигрываться
50
*, *::after, *::before {
caret-color: transparent !important;
}
Выключаем: Каретка
Chrome 57, Firefox 53, Opera 44, Safari TP38
Для других браузеров будем придумывать
что-то еще
GIF
Делаем статичным
Задача:

Сделать GIF статичным, 

то есть оставить один кадр
52
Но после пары часов 

поиска подходящего пакета,
решил попробовать написать сам 🤓
53
GIF
• Wikipedia

en.wikipedia.org/wiki/GIF
• GIF89a Specification

www.w3.org/Graphics/GIF/spec-gif89a.txt
54
Структура GIF
55
GIF Version Logical Screen Descriptor Global Color Table
Image Descriptor Block
Extension Block
Trailer
N✕
...
...
Структура GIF
56
GIF Version Logical Screen Descriptor Global Color Table
Image Descriptor Block
Extension Block
Trailer
N✕
...
...
Чтобы сделать GIF статическим,
необходимо пройтись по блокам и
оставить только первый 

Image Descriptor Block
Решение в 78 SLOC
написано за пару часов
Gist с кодом
57
Склейка CSS
Как работает Jest?
• Запускает несколько потоков (worker'ов)
• Каждый файл теста запускается в одном из
потоков, в своем контексте
• Данные между контекстами не шарятся
59
Решение
• При преобразовании CSS файла пишем
результат во временный файл-карту
• У каждого потока (процесса) свой файл
• Определенно есть решение лучше, но пока
не нашли
60
Сохраняем результирующий CSS
61
function processingCSS(data, filename) {
// получаем CSS
// сохраняем результат
const data = JSON.parse(fs.readFileSync(TMP_STYLES_FILE, 'utf8'));
data[filename] = css;
fs.writeFileSync(TMP_STYLES_FILE, JSON.stringify(data));
return css;
}
Используем
62
function generateSnapshot(data, filename) {
...
// сохраняем результат
const cssMap = JSON.parse(fs.readFileSync(TMP_STYLES_FILE, 'utf8'));
const styles = includedCssModules.map(path => cssMap[path] || '');
...
}
Собираем все вместе
Собираем все вместе
64
const htmlForScreenshot = `
<!doctype html>
<html>
<head>
<style>
*, *::after, *::before {
/* убираем динамику */
}
</style>
<style>${styles}</style>
</head>
<body>${html}</body>
</html>
`;
Итоги
• Добились своего
• Решения не идеальные, но работают
• Можно сделать лучше (надежнее) но нужно
глубже погружаться в инструменты
65
У нас все готово 🤘 

Переходим к получению скриншота...
66
Создание скриншота
Создание скриншота – сегодня
• Headless браузеры – Chrome, Firefox
• Все современные браузеры поддерживают
WebDriver
• Нужно немного кода чтобы завелось –
достаточно материала по теме
68
69
github.com/GoogleChrome/puppeteer
Делаем скриншот
70
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();
async screenshot(html) => {
const page = await browser.newPage();
await page.setContent(html);
const result = await page.screenshot({ fullPage: true });
await page.close();
return result;
}
Упрощенный код
Работает 🎉
71
Не совсем 😔
Разные версии браузера, разные ОС

= 

разный результат
72
Решение:
Делаем микро-сервис,

POST-запрос с кодом → скриншот
73
Плюсы
• Нет зависимости от машины, где
запускаются тесты
• Не требуется настроек для нового проекта
• В целом работает быстрее (всегда
разогретый браузер)
74
Минусы
• Нужно следить за "здоровьем" сервиса –
утечки памяти и баги могут его сломать
• Если ломается сервис, то тестирование
скриншотами не возможно
• В пиках нагрузки замедляется прохождение
тестов (почти не проблема, у нас облако)
75
Как выглядит «смерть» сервиса
76
Side effect:
Мы «научили» сервис отдавать
скриншот по url + CSS selector
77
Оказалось весьма полезно
• Вставляем в документацию/описание 

URL к сервису
• Получаем скриншот страницы/блока, который
будет всегда актуальным
• ...
• PROFIT!
78
Областей применения гораздо больше,
планируем развивать дальше
(но это другая история)
79
Итак, у нас есть скриншоты 🤘 

Осталось сравнить результаты...
80
Сравнение изображений
Мы используем Jest для
тестирования компонент
82
Тестирование снепшотами
83
const ReactTestRenderer = require('react-test-renderer');
test('snapshot example test', () => {
const snapshot = ReactTestRenderer.create(
<Button>test</Button>
);
expect(snapshot).toMatchSnapshot();
}
toMatchSnapshot()
• Приводит проверяемое значение к строке
• Если тест новый – сохраняет значение как есть
• Если тест не новый – сверяет новое значение 

с прежним, в случае не совпадения выводит ошибку
• Если тест пропал, то выводит что снепшот устарел
84
Сравнение изображений
• Jest не имеет встроенных инструментов для
сравнения изображений
• Хороший старт jest-image-snapshot
85
jest-image-snapshot: подключение
86
const { toMatchImageSnapshot } = require('jest-image-snapshot');
expect.extend({
toMatchImageSnapshot
});
jest-image-snapshot: использование
87
const snapshot = require('...'); // самописный хелпер
test('snapshot example test', async () => {
const snap = snapshot(<Button>test</Button>);
expect(snap.snapshot).toMatchSnapshot();
expect(await snap.screenshot).toMatchImageSnapshot();
}
Проблемы
• Недостаточно быстрый (недавно стал быстрей)
• Не учитывает режим (mode), в котором запущен
Jest, всегда создает diff-изображения
• Плохо работает с счетчиками (кол-во снепшотов)
• Не умеет считать и удалять устаревшие
изображения
88
Ключевая проблема – производительность:
Сравнение двух изображений 800х600
занимает ~1,5-2 сек
89
Сделали форк и починили
проблемы 🤓
90
Сравнение изображений
С чего начинали (~300 скриншотов):
Инициализация: 10-20 сек
Проверка: 4.5 мин
(сравнение в 3 потока)
92
🤔
«Вскрытие» показало:
каждый раз изображения сравниваются
через blink-diff (попиксельно)
93
Что нужно для сравнения
• Декодировать 2 изображения

800x600 = 480 000 пикселей х 2

4 байт х пиксель = ~2Mb x 2
• Пройтись по массивам и сравнить поэлементно
94
Библиотеки сравнения изображений (png)
• blink-diff ~1.5-2 сек
• pixelmatch ~0.5-0.7сек
• looks-same (от Gemini)
95
Как сделать быстрее?
96
Инсайт
При повторных проходах большая часть
изображений совпадает
97
Что получается
• Можно сравнить файлы без декодирования
• PNG хорошо сжимается, нужно сравнивать
несколько килобайт вместо мегабайтов
• Если файлы не равны, только тогда приступать
к сравнению попиксельно
98
Как быстро сравнить
два файла?
99
Хеш!
100
const crypto = require('crypto');
function getHash(data) {
return crypto
.createHash('md5')
.update(data, 'utf8')
.digest('hex');
}
getHash(fs.readFileSync('a.png')) === getHash(fs.readFileSync('b.png'));
// 4Kb – 58,773 ops/sec
// 137Kb – 2,186 ops/sec
Хеш!
100
const crypto = require('crypto');
function getHash(data) {
return crypto
.createHash('md5')
.update(data, 'utf8')
.digest('hex');
}
getHash(fs.readFileSync('a.png')) === getHash(fs.readFileSync('b.png'));
// 4Kb – 58,773 ops/sec
// 137Kb – 2,186 ops/sec
Overkill!
Все проще!
101
const bufferA = fs.readFileSync('a.png');
const bufferB = fs.readFileSync('b.png');
bufferA.equals(bufferB);
// 4Kb – 3,273,114 ops/sec
// 137Kb – 148,986 ops/sec
Все проще!
101
const bufferA = fs.readFileSync('a.png');
const bufferB = fs.readFileSync('b.png');
bufferA.equals(bufferB);
// 4Kb – 3,273,114 ops/sec
// 137Kb – 148,986 ops/sec
В 50-70 раз быстрее!
Сравниваем попиксельно
(своими руками)
Считаем кол-во разных пикселей
103
function countImageDiff(actualImage, expectedImage) {
const actual = png.decode(actualImage);
const actualPixels = new Uint32Array(actual.data.buffer);
const expected = png.decode(expectedImage);
const expectedPixels = new Uint32Array(expected.data.buffer);
let count = 0;
for (let i = 0; i < actualPixels.length; i++) {
if (actualPixels[i] !== expectedPixels[i]) {
count++;
}
}
return { width: actual.width, height: actual.height, count };
}
Считаем кол-во разных пикселей
104
function countImageDiff(actualImage, expectedImage) {
const actual = png.decode(actualImage);
const actualPixels = new Uint32Array(actual.data.buffer);
const expected = png.decode(expectedImage);
const expectedPixels = new Uint32Array(expected.data.buffer);
let count = 0;
for (let i = 0; i < actualPixels.length; i++) {
if (actualPixels[i] !== expectedPixels[i]) {
count++;
}
}
return { width: actual.width, height: actual.height, count };
}
Считаем кол-во разных пикселей
105
function countImageDiff(actualImage, expectedImage) {
const actual = png.decode(actualImage);
const actualPixels = new Uint32Array(actual.data.buffer);
const expected = png.decode(expectedImage);
const expectedPixels = new Uint32Array(expected.data.buffer);
let count = 0;
for (let i = 0; i < actualPixels.length; i++) {
if (actualPixels[i] !== expectedPixels[i]) {
count++;
}
}
return { width: actual.width, height: actual.height, count };
}
Считаем кол-во разных пикселей
106
function countImageDiff(actualImage, expectedImage) {
const actual = png.decode(actualImage);
const actualPixels = new Uint32Array(actual.data.buffer);
const expected = png.decode(expectedImage);
const expectedPixels = new Uint32Array(expected.data.buffer);
let count = 0;
for (let i = 0; i < actualPixels.length; i++) {
if (actualPixels[i] !== expectedPixels[i]) {
count++;
}
}
return { width: actual.width, height: actual.height, count };
}
Для двух изображений 800х600
~100 ms
107
Большая часть времени – png.decode()
Генерируем diff-изображение
Генерируем diff-изображение: как?
• Так же сравниваем попиксельно
• Создаем буфер под результирующее изображение
• Совпадающие пиксели обесцвечиваем и
засвечиваем
• Для несовпадающих пишем красный пиксель
109
Генерируем diff-изображение
110
function findImageDiff(actualImage, expectedImage) {
...
const diff = Buffer.alloc(actual.data.length);
const diffPixels = new Uint32Array(diff.buffer);
for (let i = 0; i < actualPixels.length; i++) {
if (actualPixels[i] !== expectedPixels[i]) {
count++;
diffPixels[i] = 0xff0000ff;
} else {
// обесцвечиваем и засвечиваем
}
}
return { ..., actual, expected, diff };
}
Засвечивание пикселя
111
const pixel = actualPixels[i];
const sum = ((pixel >> 0) & 0xff) + // red
((pixel >> 8) & 0xff) + // green
((pixel >> 16) & 0xff); // blue
const gray = (255 - 64 + 64 * (sum / (255 + 255 + 255))) | 0;
// alpha blue green red
diffPixels[i] = (pixel & 0xff000000) | gray << 16 | gray << 8 | gray;
«Загоняем» пиксели
в этот диапазон
Сохраняем diff-изображение
112
const png = require('fast-png');
const {
width,
height,
actual,
expected,
diff
} = findImageDiff(actualImage, expectedImage);
fs.writeFileSync(diffImageFilename, png.encode({
width,
height: height * 3,
data: Buffer.concat([
expected.data,
diff,
actual.data
])
}));
Результат
113
Эталонное изображение
Новое изображение
Diff (разница)
Для двух изображений 800х600
~250 ms
114
Идея для«стартапа»
Запилить decode, сравнение и decode PNG

на WebAssembly – вероятно будет быстрее
115
Важно☝
Делать diff-изображение не нужно, т.к. многие
инструменты имеют встроенное сравнение
изображений
116
Например
117
Например
117
Создаем diff-изображение, только если
пользователь явно попросил об этом
118
jest --expand
Можно ли сделать еще быстрее?
Инсайт
Для одного и того же кода – тот же скриншот 

В большинстве случаев 

можно не делать запрос к сервису
120
План
• Генерируем HTML код
• Если есть изображение и его код совпадает 

с сгенерированным – не делаем запроса, используем
имеющееся изображение
• Если код различается или нет изображения – делаем
запрос к сервису
• Сохраняем код, которым получаем изображение
121
Где хранить код?
Можно в самом изображении :)
122
Похожий сценарий 😉
• Читаем Wikipedia

en.wikipedia.org/wiki/Portable_Network_Graphics
• Читаем код библиотек
• ...
• Умеем работать с PNG
123
В PNG может хранится не только
графическая информация
Можно хранить произвольные данные
124
Решение (~70 SLOC)
Не полетело, так как файлы получаются разными но
визуальной разницы нет
В результате, храним код скриншота
отдельным файлом
125
Что дало решение?
126
Проверка ~300 изображений 800x600
45-50 сек → 12-15 сек
(полная проверка, локальный запуск)
☝
Бонус: значительно снизили нагрузку 

на сервис скриншотов
127
Хранение изображений в GIT
Проблема
Бинарные данные хранятся в истории
целиком – история быстро растет
129
GIT LFS
130
GIT LFS
• Включаем в репозитарии GIT LFS
• Определяем файлы, которые нужно хранить в хранилище в
.gitattributes

*.png filter=lfs diff=lfs merge=lfs -text
• Инициализируем: git lfs install
• PROFIT: в истории хранятся только хеши файлов, локально
реальные файлы, git push/pull работают прозрачно
131
GIT LFS: профит
• В истории git'а хранятся только хеши файлов
• Локально хранятся реальные файлы
• git push/pull работают как обычно и делают
всю магию
132
CI или beyond the frontend
134
Прохождение всех unit-тестов на CI
~4 сек 🤘
135
Но полный время выполнения на CI
~3,5 мин 😞
136
Полез ковырять 🤓
(с нулевыми знаниями Teamcity)
Что происходит внутри CI
• git checkout
• npm install
• jest --ci
• eslint
• stylelint
137
Что происходит внутри CI
• git checkout
• npm install
• jest --ci
• eslint
• stylelint
137
Тюним настройки, добавляем кеши
3,5 мин → <30 сек
Результаты
Время
• Локально прохождение всех юнит тестов

несколько минут → 12-15 секунд
• Прохождение всех проверок на CI:

несколько минут → 20-30 секунд
139
Свой плагин для jest
• Быстрый, поддерживает digest
• Учитывает режим (mode), в котором запущен Jest
• Создает diff-изображения только если --expand
• Правильно работает с счетчиками
• Умеет считать и удалять устаревшие изображения
140
Будущее плагина:
• План А: попробуем предложить в Jest
• План Б:
• Либо смержим с jest-image-snapshot
• Либо опубликуем как самостоятельный пакет
141
142
Про остальное
Как обкатаем подумаем о том,

чтобы опубликовать в Open Source
Подводим итоги
Итоги
• Неколько плагинов:

для Babel, CSS Modules и Jest
• Все под контролем, мы знаем как работают
все части решения
• Время unit-тестов не «пострадало» 

от добавления тестирования скриншотами
144
Время
145
Со скриншотамиБез скриншотов
328 изображений
проверено, что они актуальны
Работа над решениями привела 

к новым областям применения скриншотов
(использование в документации/инструментах)
146
Чему мы научились
• Как работает Jest внутри
• Как устроены форматы GIF и PNG
• Лучше понимание Buffer API
• Как настраивать Teamcity
• Как сделать юнит-тесты скриншотами быстрыми :)
147
Юнит-тестирование скриншотами
может быть со скоростью пули
148
Не бойтесь делать 

свои решения!
149
Роман Дворнов
@rdvornov
rdvornov@gmail.com
Спасибо!

More Related Content

PDF
SPA инструменты
PDF
Баба Яга против!
PDF
Быстро о быстром
PDF
Не бойся, это всего лишь данные... просто их много
PDF
DOM-шаблонизаторы – не только "быстро"
PDF
Basis.js – «под капотом»
PDF
Опыт разработки эффективного SPA
PDF
Инструментируй это
SPA инструменты
Баба Яга против!
Быстро о быстром
Не бойся, это всего лишь данные... просто их много
DOM-шаблонизаторы – не только "быстро"
Basis.js – «под капотом»
Опыт разработки эффективного SPA
Инструментируй это

What's hot (20)

PDF
Иван Карев — Клиентская оптимизация
PDF
Как сделать Instagram в браузере — Дмитрий Дудин, xbSoftware
PPT
непрерывная интеграция шаг к непрерывному деплою родионов игорь
PDF
Иван Карев — Клиентская оптимизация
PDF
Баба-Яга против! — Роман Дворнов, Ostrovok.ru
PDF
Introduction in Node.js (in russian)
PDF
Инструменты разные нужны, инструменты разные важны
PDF
Изоморфный JavaScript — будущее уже здесь
PDF
Парсим CSS
PPTX
Ruby - или зачем мне еще один язык программирования?
PPTX
Chef @DevWeb
PDF
М. Боднарчук Современное функциональное тестирование с Codeception
PDF
«​Масштабируемый DevOps​» Александр Колесень
PDF
DevConf 2012 - Yii, его разработка и Yii2
PDF
"Инструментарий разработчика iOS: Xcode, AppCode и сторонние инструменты". Ма...
PDF
UWDC 2013, Как мы используем Yii
PPTX
Михаил Боднарчук Современное функциональное тестирование с Codeception
PDF
«Организация Frontend-разработки на крупном проекте» — Дмитрий Кузнецов
PDF
Виталий Харисов - Система ведения задач
PDF
Backbone.js
Иван Карев — Клиентская оптимизация
Как сделать Instagram в браузере — Дмитрий Дудин, xbSoftware
непрерывная интеграция шаг к непрерывному деплою родионов игорь
Иван Карев — Клиентская оптимизация
Баба-Яга против! — Роман Дворнов, Ostrovok.ru
Introduction in Node.js (in russian)
Инструменты разные нужны, инструменты разные важны
Изоморфный JavaScript — будущее уже здесь
Парсим CSS
Ruby - или зачем мне еще один язык программирования?
Chef @DevWeb
М. Боднарчук Современное функциональное тестирование с Codeception
«​Масштабируемый DevOps​» Александр Колесень
DevConf 2012 - Yii, его разработка и Yii2
"Инструментарий разработчика iOS: Xcode, AppCode и сторонние инструменты". Ма...
UWDC 2013, Как мы используем Yii
Михаил Боднарчук Современное функциональное тестирование с Codeception
«Организация Frontend-разработки на крупном проекте» — Дмитрий Кузнецов
Виталий Харисов - Система ведения задач
Backbone.js
Ad

Similar to Unit-тестирование скриншотами: преодолеваем звуковой барьер (20)

PDF
Жизнь в изоляции
PPTX
Построение собственного JS SDK — зачем и как?
PDF
CSSO — минимизируем CSS
PPTX
Codeception Introduction
PDF
Суперсилы Chrome DevTools — Роман Сальников, 2ГИС
PDF
Регрессионное тестирование верстки
PDF
My Open Source (Sept 2017)
PPT
Easy authcache 2 кеширование для pro родионов игорь
PDF
"Webpack: 7 бед — один ответ" — Денис Измайлов, MoscowJS 17
PPT
Easy authcache 2 кэширование для pro. Родионов Игорь
PDF
Жизнь в изоляции / Роман Дворнов (Avito)
PDF
Вадим Макишвили "Вёрстка в IntelliJIDEA"
PDF
Олег Мохов "Драматическая история одной маленькой промостранички"
PDF
Работа со статикой в Django
PDF
Сергей Бережной, Варвара Степанова "Как использовать БЭМ! вне Яндекса"
PDF
Модульная архитектура Сбербанк Онлайн, Владимир Озеров и Александр Черушнико...
PPTX
Codeception UATestingDays
PDF
Евгений Батовский, Николай Птущук "Современный станок верстальщика"
PDF
Cовременный станок верстальщика
Жизнь в изоляции
Построение собственного JS SDK — зачем и как?
CSSO — минимизируем CSS
Codeception Introduction
Суперсилы Chrome DevTools — Роман Сальников, 2ГИС
Регрессионное тестирование верстки
My Open Source (Sept 2017)
Easy authcache 2 кеширование для pro родионов игорь
"Webpack: 7 бед — один ответ" — Денис Измайлов, MoscowJS 17
Easy authcache 2 кэширование для pro. Родионов Игорь
Жизнь в изоляции / Роман Дворнов (Avito)
Вадим Макишвили "Вёрстка в IntelliJIDEA"
Олег Мохов "Драматическая история одной маленькой промостранички"
Работа со статикой в Django
Сергей Бережной, Варвара Степанова "Как использовать БЭМ! вне Яндекса"
Модульная архитектура Сбербанк Онлайн, Владимир Озеров и Александр Черушнико...
Codeception UATestingDays
Евгений Батовский, Николай Птущук "Современный станок верстальщика"
Cовременный станок верстальщика
Ad

More from Roman Dvornov (17)

PDF
Масштабируемая архитектура фронтенда
PDF
CSS глазами машин
PDF
Rempl – крутая платформа для крутых инструментов
PDF
Remote (dev)tools своими руками
PDF
Как сделать ваш JavaScript быстрее
PDF
CSS parsing: performance tips & tricks
PDF
Парсим CSS: performance tips & tricks
PDF
CSSO – история ускорения
PDF
CSSO – compress CSS (english version)
PDF
CSSO — сжимаем CSS (часть 2)
PDF
Component Inspector
PDF
Инструменты разные нужны, инструменты разные важны
PDF
Карточный домик
PDF
Как построить DOM
PDF
Компонентный подход: скучно, неинтересно, бесперспективно
PDF
Basis.js - почему я не бросил разрабатывать свой фреймворк (extended)
PDF
basis.js - почему я не бросил разрабатывать свой фреймворк
Масштабируемая архитектура фронтенда
CSS глазами машин
Rempl – крутая платформа для крутых инструментов
Remote (dev)tools своими руками
Как сделать ваш JavaScript быстрее
CSS parsing: performance tips & tricks
Парсим CSS: performance tips & tricks
CSSO – история ускорения
CSSO – compress CSS (english version)
CSSO — сжимаем CSS (часть 2)
Component Inspector
Инструменты разные нужны, инструменты разные важны
Карточный домик
Как построить DOM
Компонентный подход: скучно, неинтересно, бесперспективно
Basis.js - почему я не бросил разрабатывать свой фреймворк (extended)
basis.js - почему я не бросил разрабатывать свой фреймворк

Unit-тестирование скриншотами: преодолеваем звуковой барьер