Полный цикл тестирования
React-приложений
Алексей Андросов, ведущий разработчик, Авто.ру
Наталья Стусь, разработчик интерфейсов, Авто.ру
MosQA meetup, 23 октября 2019, Москва
Frontend
2
Монорепозиторий для нескольких отдельных проектов
(desktop & mobile web)

+ +
• Классифайд авто • Классифайд запчастей
• Поиск автосервисов • Отзывы на автомобили
• Личный кабинет • Кабинет дилера
Релизный цикл
3
1. Разработка фичи в отдельной ветке
2. Тестирование фичи в отдельной ветке
Код не попадает в RC, пока задача не готова
3. Сборка RC из проверенных веток
В случае конфликтов придется перепроверять фичи
4. Ручной регресс целевого пакета
Несколько сотен тест-кейсов для каждого проекта
5. Релиз
6. Вливание ветки в мастер
Общий код может задеть другие проекты в репозитории
Автотесты (e2e) Selenium + java
4
Ускорили регресс (1 час, а не 10 часов)
Сняли рутину с ручных тестировщиков
Проще воспроизводить найденные баги
Обеспечили стабильность покрытия
Повысили точность тестирования UI
Нестабильное окружение — нестабильные тесты
Новые фичи покрываются с задержкой
Покрыты только критичные сценарии
Разрабатывает отдельная команда
Сложно разбирать упавшие тесты
Написали около 5000 e2e тестов
Автотесты (e2e) Selenium + java
5
Автотесты (e2e) Selenium + java
6
Автотесты (e2e) Selenium + java
7
Пытаемся решить проблемы
8
› Часть тестов запускается на проде
› Часть тестов перевели на моки https://github.com/doochik/mockritsa
› Увеличиваем мощности для запуска тестов
Для CI такие тесты не подходят. Решать эти проблемы — дорого!
Чего нам не хватает?
9
› Быстрые тесты (несколько минут)
› Стабильные тесты (0 ложных падений)
› Запуск тестов на PR
› Запуск без тестовых стендов, настройки бекендов и баз, создания
тестовых данных
› Написание тестов одновременно с кодом
› Удобная интерпретация результатов тестов
Инструменты
10
Модульные тесты: Jest + Enzyme
Интеграционные тесты: Jest + Enzyme
End-to-end тесты: Jest + Puppeteer / Selenium
Тесты на отдельные функции (Jest)
11
const moment = require('moment');
function getNearestHourStart() {
    const now = moment();
    if (now.minutes() >= 30) {
        now.add(1, 'h');
    }
    return now.startOf('h').format('HH:mm');
}
module.exports = getNearestHourStart;
const getNearestHourStart = require('./getNearestHourStart');
const MockDate = require('mockdate');
it('если меньше 30 минут то округляет до начала предыдущего 
часа', () => {
    MockDate.set('2019-03-01 13:13:13');
    const result = getNearestHourStart();
    expect(result).toBe('13:00');
});
it('если больше 30 минут то округляет до начала следующего 
часа', () => {
    MockDate.set('2019-03-01 13:43:13');
    const result = getNearestHourStart();
    expect(result).toBe('14:00');
});
it('добавит 0 впереди если часов меньше 10', () => {
    MockDate.set('2019-03-01 03:43:13');
    const result = getNearestHourStart();
    expect(result).toBe('04:00');
});
Тесты на отдельные компоненты (Enzyme)
12
const React = require('react');
const { shallow } = require('enzyme');
const { shallowToJson } = require('enzyme-to-json');
const BanMessage = require('./BanMessage');
it('должен правильно отрендерить причины бана', () => {
    const wrapper = shallow(
        <BanMessage
            bunker={{
                banReasons: {
                    foo: {
                        text_user_ban: 'foo ban text',
                    },
                    bar: {
                        text_user_ban: 'bar ban text',
                    },
                },
            }}
            className="Sales__banned"
            reasons={ [ 'foo', 'bar' ] }
        />);
    expect(shallowToJson(wrapper)).toMatchSnapshot();
});
exports[`должен правильно отрендерить причины бана 1`] = `
<InfoMessage
title="Личный кабинет заблокирован"
type="Error"
>
<div className=«BanMessage__Part» key=«0» >
"foo ban text"
</div>
<div className=«BanMessage__Part» key=«1»>
"bar ban text"
</div>
<div className="BanMessage__Buttons">
<Connect(SupportMessage)>
<Button
className="BanMessage__Button"
url="//auto.ru/?chat"
>
Написать в поддержку
</Button>
</Connect(SupportMessage)>
</div>
</InfoMessage>
`;
Тестирование верстки компонентов
13
› Покрыли тестами логику компонента
› Хотим по той же схеме проверять верстку компонентов!
Идеальный мир
14
it("should render MyComponent", () => {
const snapshot = render(<MyComponent/>);
expect(snapshot).toMatchImageSnapshot();
});
Тестирование верстки компонентов
15
Поиск по интернетам дает один ответ: puppeteer
const page = await browser.newPage();
await page.goto('http://localhost/page.html');
const screenshot = await page.screenshot();
expect(screenshot).toMatchImageSnapshot()
Что такое page.html?
Мы же хотим протестировать отдельный компонент!
Чего мы хотим?
16
› Тестировать отдельный компонент без сборки и запуска всего проекта
› Сборка компонента для теста не отличается от сборки для продакшена
› Простота развертывания для разработчика
› Простота использования для разработчика
jest-puppeteer-react
17
Как работает:
› Ищет все файлы с тестами *.browser.js
› Берет ваш конфиг webpack и собирает компоненты из тестов
› Пакует все в html через html-webpack-plugin
› Рендерит отдельные компоненты в браузере и снимает с них
скриншоты
jest-puppeteer-react
18
Что умеет:
› Cнимать скриншоты, сравнивать их с эталоном (jest-image-snapshot) и
показывать разницу
› Менять viewport (тестируем media-query), исполнять js в консоли — все
что умеет puppeteer
› Отключать headless-режим для отладки
› Запускать хром в docker для использования на CI
Пример скриншотного теста
19
const { render } = require('jest-puppeteer-react');
const { Provider } = require('react-redux');
const BanMessage = require('./BanMessage');
it('должен отрендерить причины бана', async() => {
await render(
<Provider store={ /* redux store mock */ }>
<BanMessage
bunker={{
banReasons: {
foo: { text_user_ban: 'foo ban text' },
bar: { text_user_ban: 'bar ban text' },
},
}}
reasons={ [ 'foo', 'bar' ] }
/>
</Provider>,
{ viewport: { width: 1220, height: 300 } }
);
const screenshot = await page.screenshot();
expect(screenshot).toMatchImageSnapshot();
});
Пример скриншотного теста
20
Эталон Результат теста
Пример скриншотного теста
21
Результат сравнения
с подсветкой изменений
jest-puppeteer-react
22
Почему падают тесты?
1. Первая причина - это ты.
2. А вторая - это тоже ты.
3. Ну вы поняли…
jest-puppeteer-react
23
Частые причина флапа тестов:
› У компонента есть анимация
› Изменилась или сфейлилась внешняя зависимость
› Не замокали дату, рандом и т.п.
jest-puppeteer-react
24
Нюансы:
› Скриншотные тесты дорогие. Логику тестируем в jest+enzyme, а тут
только CSS.
› Скорость тестов больше зависит от скорости работы самих компонент,
а не от количества тестов.
› Мы исследуем идеи создания кластера из контейнеров. docker-
контейнер с хромом не получится бесконечно вертикально
масштабировать.
› Хотим попробовать на основе дерева зависимостей запускать тесты
только для затронутых компонент.
Что получилось
25
Полное покрытие компонентов юнит-тестами:
› логика (jest)
› html-верстка (enzyme)
› css-верстка (jest-puppeteer-react)
Что получилось
26
› Все изменения в коде покрываются тестами
› Тесты лежат рядом с кодом
› Запускаем на PR
› Разработчик сразу видит фидбек
› Удобнее делать code review
› Тесты работают как документация
› Разработчики полюбили писать тесты
Что получилось
27
› Проверяем больше тест-кейсов
› Меньше реопенов от тестирования
› Часть задач не тестируются вручную
› Задачи быстрее попадают на прод
› Приближаем мечту релиза «по кнопке»
› За качество отвечает разработчик!
Релизный цикл
28
1. Разработка фичи в отдельной ветке
2. Покрытие кода тестами в процессе разработки
3. Запуск тестов на PR
4. Ручное тестирование фичи (не всегда!)
5. Прогон e2e тестов
6. Релиз
7. Вливание ветки в мастер
Дальнейшие планы
29
› Ускорение скриншотных тестов
› Поддержка e2e-тестов силами разработчиков
› Релиз «по кнопке»
Спасибо
Алексей Андросов
Ведущий разработчик, Авто.ру
Наталья Стусь
Разработчик интерфейсов, Авто.ру
aandrosov@yandex-team.ru
@doochik
natix@yandex-team.ru
@ArminaAiren

Полный цикл тестирования React-приложений, Алексей Андросов и Наталья Стусь

  • 1.
    Полный цикл тестирования React-приложений АлексейАндросов, ведущий разработчик, Авто.ру Наталья Стусь, разработчик интерфейсов, Авто.ру MosQA meetup, 23 октября 2019, Москва
  • 2.
    Frontend 2 Монорепозиторий для несколькихотдельных проектов (desktop & mobile web)
 + + • Классифайд авто • Классифайд запчастей • Поиск автосервисов • Отзывы на автомобили • Личный кабинет • Кабинет дилера
  • 3.
    Релизный цикл 3 1. Разработкафичи в отдельной ветке 2. Тестирование фичи в отдельной ветке Код не попадает в RC, пока задача не готова 3. Сборка RC из проверенных веток В случае конфликтов придется перепроверять фичи 4. Ручной регресс целевого пакета Несколько сотен тест-кейсов для каждого проекта 5. Релиз 6. Вливание ветки в мастер Общий код может задеть другие проекты в репозитории
  • 4.
    Автотесты (e2e) Selenium+ java 4 Ускорили регресс (1 час, а не 10 часов) Сняли рутину с ручных тестировщиков Проще воспроизводить найденные баги Обеспечили стабильность покрытия Повысили точность тестирования UI Нестабильное окружение — нестабильные тесты Новые фичи покрываются с задержкой Покрыты только критичные сценарии Разрабатывает отдельная команда Сложно разбирать упавшие тесты Написали около 5000 e2e тестов
  • 5.
  • 6.
  • 7.
  • 8.
    Пытаемся решить проблемы 8 ›Часть тестов запускается на проде › Часть тестов перевели на моки https://github.com/doochik/mockritsa › Увеличиваем мощности для запуска тестов Для CI такие тесты не подходят. Решать эти проблемы — дорого!
  • 9.
    Чего нам нехватает? 9 › Быстрые тесты (несколько минут) › Стабильные тесты (0 ложных падений) › Запуск тестов на PR › Запуск без тестовых стендов, настройки бекендов и баз, создания тестовых данных › Написание тестов одновременно с кодом › Удобная интерпретация результатов тестов
  • 10.
    Инструменты 10 Модульные тесты: Jest+ Enzyme Интеграционные тесты: Jest + Enzyme End-to-end тесты: Jest + Puppeteer / Selenium
  • 11.
    Тесты на отдельныефункции (Jest) 11 const moment = require('moment'); function getNearestHourStart() {     const now = moment();     if (now.minutes() >= 30) {         now.add(1, 'h');     }     return now.startOf('h').format('HH:mm'); } module.exports = getNearestHourStart; const getNearestHourStart = require('./getNearestHourStart'); const MockDate = require('mockdate'); it('если меньше 30 минут то округляет до начала предыдущего  часа', () => {     MockDate.set('2019-03-01 13:13:13');     const result = getNearestHourStart();     expect(result).toBe('13:00'); }); it('если больше 30 минут то округляет до начала следующего  часа', () => {     MockDate.set('2019-03-01 13:43:13');     const result = getNearestHourStart();     expect(result).toBe('14:00'); }); it('добавит 0 впереди если часов меньше 10', () => {     MockDate.set('2019-03-01 03:43:13');     const result = getNearestHourStart();     expect(result).toBe('04:00'); });
  • 12.
    Тесты на отдельныекомпоненты (Enzyme) 12 const React = require('react'); const { shallow } = require('enzyme'); const { shallowToJson } = require('enzyme-to-json'); const BanMessage = require('./BanMessage'); it('должен правильно отрендерить причины бана', () => {     const wrapper = shallow(         <BanMessage             bunker={{                 banReasons: {                     foo: {                         text_user_ban: 'foo ban text',                     },                     bar: {                         text_user_ban: 'bar ban text',                     },                 },             }}             className="Sales__banned"             reasons={ [ 'foo', 'bar' ] }         />);     expect(shallowToJson(wrapper)).toMatchSnapshot(); }); exports[`должен правильно отрендерить причины бана 1`] = ` <InfoMessage title="Личный кабинет заблокирован" type="Error" > <div className=«BanMessage__Part» key=«0» > "foo ban text" </div> <div className=«BanMessage__Part» key=«1»> "bar ban text" </div> <div className="BanMessage__Buttons"> <Connect(SupportMessage)> <Button className="BanMessage__Button" url="//auto.ru/?chat" > Написать в поддержку </Button> </Connect(SupportMessage)> </div> </InfoMessage> `;
  • 13.
    Тестирование верстки компонентов 13 ›Покрыли тестами логику компонента › Хотим по той же схеме проверять верстку компонентов!
  • 14.
    Идеальный мир 14 it("should renderMyComponent", () => { const snapshot = render(<MyComponent/>); expect(snapshot).toMatchImageSnapshot(); });
  • 15.
    Тестирование верстки компонентов 15 Поискпо интернетам дает один ответ: puppeteer const page = await browser.newPage(); await page.goto('http://localhost/page.html'); const screenshot = await page.screenshot(); expect(screenshot).toMatchImageSnapshot() Что такое page.html? Мы же хотим протестировать отдельный компонент!
  • 16.
    Чего мы хотим? 16 ›Тестировать отдельный компонент без сборки и запуска всего проекта › Сборка компонента для теста не отличается от сборки для продакшена › Простота развертывания для разработчика › Простота использования для разработчика
  • 17.
    jest-puppeteer-react 17 Как работает: › Ищетвсе файлы с тестами *.browser.js › Берет ваш конфиг webpack и собирает компоненты из тестов › Пакует все в html через html-webpack-plugin › Рендерит отдельные компоненты в браузере и снимает с них скриншоты
  • 18.
    jest-puppeteer-react 18 Что умеет: › Cниматьскриншоты, сравнивать их с эталоном (jest-image-snapshot) и показывать разницу › Менять viewport (тестируем media-query), исполнять js в консоли — все что умеет puppeteer › Отключать headless-режим для отладки › Запускать хром в docker для использования на CI
  • 19.
    Пример скриншотного теста 19 const{ render } = require('jest-puppeteer-react'); const { Provider } = require('react-redux'); const BanMessage = require('./BanMessage'); it('должен отрендерить причины бана', async() => { await render( <Provider store={ /* redux store mock */ }> <BanMessage bunker={{ banReasons: { foo: { text_user_ban: 'foo ban text' }, bar: { text_user_ban: 'bar ban text' }, }, }} reasons={ [ 'foo', 'bar' ] } /> </Provider>, { viewport: { width: 1220, height: 300 } } ); const screenshot = await page.screenshot(); expect(screenshot).toMatchImageSnapshot(); });
  • 20.
  • 21.
    Пример скриншотного теста 21 Результатсравнения с подсветкой изменений
  • 22.
    jest-puppeteer-react 22 Почему падают тесты? 1.Первая причина - это ты. 2. А вторая - это тоже ты. 3. Ну вы поняли…
  • 23.
    jest-puppeteer-react 23 Частые причина флапатестов: › У компонента есть анимация › Изменилась или сфейлилась внешняя зависимость › Не замокали дату, рандом и т.п.
  • 24.
    jest-puppeteer-react 24 Нюансы: › Скриншотные тестыдорогие. Логику тестируем в jest+enzyme, а тут только CSS. › Скорость тестов больше зависит от скорости работы самих компонент, а не от количества тестов. › Мы исследуем идеи создания кластера из контейнеров. docker- контейнер с хромом не получится бесконечно вертикально масштабировать. › Хотим попробовать на основе дерева зависимостей запускать тесты только для затронутых компонент.
  • 25.
    Что получилось 25 Полное покрытиекомпонентов юнит-тестами: › логика (jest) › html-верстка (enzyme) › css-верстка (jest-puppeteer-react)
  • 26.
    Что получилось 26 › Всеизменения в коде покрываются тестами › Тесты лежат рядом с кодом › Запускаем на PR › Разработчик сразу видит фидбек › Удобнее делать code review › Тесты работают как документация › Разработчики полюбили писать тесты
  • 27.
    Что получилось 27 › Проверяембольше тест-кейсов › Меньше реопенов от тестирования › Часть задач не тестируются вручную › Задачи быстрее попадают на прод › Приближаем мечту релиза «по кнопке» › За качество отвечает разработчик!
  • 28.
    Релизный цикл 28 1. Разработкафичи в отдельной ветке 2. Покрытие кода тестами в процессе разработки 3. Запуск тестов на PR 4. Ручное тестирование фичи (не всегда!) 5. Прогон e2e тестов 6. Релиз 7. Вливание ветки в мастер
  • 29.
    Дальнейшие планы 29 › Ускорениескриншотных тестов › Поддержка e2e-тестов силами разработчиков › Релиз «по кнопке»
  • 30.
    Спасибо Алексей Андросов Ведущий разработчик,Авто.ру Наталья Стусь Разработчик интерфейсов, Авто.ру aandrosov@yandex-team.ru @doochik natix@yandex-team.ru @ArminaAiren