SlideShare una empresa de Scribd logo
1 de 154
Descargar para leer sin conexión
Юнит-тестирование скриншотами:
преодолеваем звуковой барьер
Роман Дворнов
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
Спасибо!

Más contenido relacionado

La actualidad más candente

Иван Карев — Клиентская оптимизация
Иван Карев — Клиентская оптимизацияИван Карев — Клиентская оптимизация
Иван Карев — Клиентская оптимизацияYandex
 
Как сделать Instagram в браузере — Дмитрий Дудин, xbSoftware
Как сделать Instagram в браузере — Дмитрий Дудин, xbSoftwareКак сделать Instagram в браузере — Дмитрий Дудин, xbSoftware
Как сделать Instagram в браузере — Дмитрий Дудин, xbSoftwareYandex
 
непрерывная интеграция шаг к непрерывному деплою родионов игорь
непрерывная интеграция   шаг к непрерывному деплою родионов игорьнепрерывная интеграция   шаг к непрерывному деплою родионов игорь
непрерывная интеграция шаг к непрерывному деплою родионов игорьdrupalconf
 
Иван Карев — Клиентская оптимизация
Иван Карев — Клиентская оптимизацияИван Карев — Клиентская оптимизация
Иван Карев — Клиентская оптимизацияYandex
 
Баба-Яга против! — Роман Дворнов, Ostrovok.ru
Баба-Яга против! — Роман Дворнов, Ostrovok.ruБаба-Яга против! — Роман Дворнов, Ostrovok.ru
Баба-Яга против! — Роман Дворнов, Ostrovok.ruYandex
 
Introduction in Node.js (in russian)
Introduction in Node.js (in russian)Introduction in Node.js (in russian)
Introduction in Node.js (in russian)Mikhail Davydov
 
Инструменты разные нужны, инструменты разные важны
Инструменты разные нужны, инструменты разные важныИнструменты разные нужны, инструменты разные важны
Инструменты разные нужны, инструменты разные важныCodeFest
 
Изоморфный JavaScript — будущее уже здесь
Изоморфный JavaScript — будущее уже здесьИзоморфный JavaScript — будущее уже здесь
Изоморфный JavaScript — будущее уже здесьCodeFest
 
Ruby - или зачем мне еще один язык программирования?
Ruby - или зачем мне еще один язык программирования?Ruby - или зачем мне еще один язык программирования?
Ruby - или зачем мне еще один язык программирования?Pavel Tsukanov
 
М. Боднарчук Современное функциональное тестирование с Codeception
М. Боднарчук Современное функциональное тестирование с CodeceptionМ. Боднарчук Современное функциональное тестирование с Codeception
М. Боднарчук Современное функциональное тестирование с CodeceptionAlbina Tiupa
 
«​Масштабируемый DevOps​» Александр Колесень
«​Масштабируемый DevOps​» Александр Колесень«​Масштабируемый DevOps​» Александр Колесень
«​Масштабируемый DevOps​» Александр КолесеньIT Share
 
DevConf 2012 - Yii, его разработка и Yii2
DevConf 2012 - Yii, его разработка и Yii2DevConf 2012 - Yii, его разработка и Yii2
DevConf 2012 - Yii, его разработка и Yii2Alexander Makarov
 
"Инструментарий разработчика iOS: Xcode, AppCode и сторонние инструменты". Ма...
"Инструментарий разработчика iOS: Xcode, AppCode и сторонние инструменты". Ма..."Инструментарий разработчика iOS: Xcode, AppCode и сторонние инструменты". Ма...
"Инструментарий разработчика iOS: Xcode, AppCode и сторонние инструменты". Ма...Yandex
 
UWDC 2013, Как мы используем Yii
UWDC 2013, Как мы используем YiiUWDC 2013, Как мы используем Yii
UWDC 2013, Как мы используем YiiAlexander Makarov
 
Михаил Боднарчук Современное функциональное тестирование с Codeception
Михаил Боднарчук Современное функциональное тестирование с CodeceptionМихаил Боднарчук Современное функциональное тестирование с Codeception
Михаил Боднарчук Современное функциональное тестирование с CodeceptionAlbina Tiupa
 
«Организация Frontend-разработки на крупном проекте» — Дмитрий Кузнецов
«Организация Frontend-разработки на крупном проекте» — Дмитрий Кузнецов«Организация Frontend-разработки на крупном проекте» — Дмитрий Кузнецов
«Организация Frontend-разработки на крупном проекте» — Дмитрий Кузнецов2ГИС Технологии
 
Виталий Харисов - Система ведения задач
Виталий Харисов - Система ведения задачВиталий Харисов - Система ведения задач
Виталий Харисов - Система ведения задачYandex
 

La actualidad más candente (20)

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

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

Жизнь в изоляции
Жизнь в изоляцииЖизнь в изоляции
Жизнь в изоляцииRoman Dvornov
 
Построение собственного JS SDK — зачем и как?
Построение собственного JS SDK — зачем и как?Построение собственного JS SDK — зачем и как?
Построение собственного JS SDK — зачем и как?buranLcme
 
CSSO — минимизируем CSS
 CSSO — минимизируем CSS CSSO — минимизируем CSS
CSSO — минимизируем CSSRoman Dvornov
 
Суперсилы Chrome DevTools — Роман Сальников, 2ГИС
Суперсилы Chrome DevTools — Роман Сальников, 2ГИССуперсилы Chrome DevTools — Роман Сальников, 2ГИС
Суперсилы Chrome DevTools — Роман Сальников, 2ГИСYandex
 
Регрессионное тестирование верстки
Регрессионное тестирование версткиРегрессионное тестирование верстки
Регрессионное тестирование версткиTalks&Works
 
My Open Source (Sept 2017)
My Open Source (Sept 2017)My Open Source (Sept 2017)
My Open Source (Sept 2017)Roman Dvornov
 
Easy authcache 2 кеширование для pro родионов игорь
Easy authcache 2   кеширование для pro родионов игорьEasy authcache 2   кеширование для pro родионов игорь
Easy authcache 2 кеширование для pro родионов игорьdrupalconf
 
"Webpack: 7 бед — один ответ" — Денис Измайлов, MoscowJS 17
"Webpack: 7 бед — один ответ" — Денис Измайлов, MoscowJS 17"Webpack: 7 бед — один ответ" — Денис Измайлов, MoscowJS 17
"Webpack: 7 бед — один ответ" — Денис Измайлов, MoscowJS 17MoscowJS
 
Easy authcache 2 кэширование для pro. Родионов Игорь
Easy authcache 2   кэширование для pro. Родионов ИгорьEasy authcache 2   кэширование для pro. Родионов Игорь
Easy authcache 2 кэширование для pro. Родионов ИгорьPVasili
 
Жизнь в изоляции / Роман Дворнов (Avito)
Жизнь в изоляции / Роман Дворнов (Avito)Жизнь в изоляции / Роман Дворнов (Avito)
Жизнь в изоляции / Роман Дворнов (Avito)Ontico
 
Вадим Макишвили "Вёрстка в IntelliJIDEA"
Вадим Макишвили "Вёрстка в IntelliJIDEA"Вадим Макишвили "Вёрстка в IntelliJIDEA"
Вадим Макишвили "Вёрстка в IntelliJIDEA"Yandex
 
Олег Мохов "Драматическая история одной маленькой промостранички"
Олег Мохов "Драматическая история одной маленькой промостранички"Олег Мохов "Драматическая история одной маленькой промостранички"
Олег Мохов "Драматическая история одной маленькой промостранички"Yandex
 
Работа со статикой в Django
Работа со статикой в DjangoРабота со статикой в Django
Работа со статикой в DjangoMoscowDjango
 
Сергей Бережной, Варвара Степанова "Как использовать БЭМ! вне Яндекса"
Сергей Бережной, Варвара Степанова "Как использовать БЭМ! вне Яндекса"Сергей Бережной, Варвара Степанова "Как использовать БЭМ! вне Яндекса"
Сергей Бережной, Варвара Степанова "Как использовать БЭМ! вне Яндекса"Yandex
 
Модульная архитектура Сбербанк Онлайн, Владимир Озеров и Александр Черушнико...
Модульная архитектура Сбербанк Онлайн, Владимир Озеров и Александр Черушнико...Модульная архитектура Сбербанк Онлайн, Владимир Озеров и Александр Черушнико...
Модульная архитектура Сбербанк Онлайн, Владимир Озеров и Александр Черушнико...Сбертех | SberTech
 
Codeception UATestingDays
Codeception UATestingDaysCodeception UATestingDays
Codeception UATestingDaysdavertmik
 
Cовременный станок верстальщика
Cовременный станок верстальщикаCовременный станок верстальщика
Cовременный станок верстальщикаmcslayer
 
Евгений Батовский, Николай Птущук "Современный станок верстальщика"
Евгений Батовский, Николай Птущук "Современный станок верстальщика"Евгений Батовский, Николай Птущук "Современный станок верстальщика"
Евгений Батовский, Николай Птущук "Современный станок верстальщика"Yandex
 

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

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

Más de Roman Dvornov

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

Más de Roman Dvornov (17)

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

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