Хабрахабр

Полный цикл тестирования React-приложений. Доклад Авто.ру

Стремление уйти от ручного регрессионого тестирования — хороший повод внедрить автотесты. Вопрос, какие именно? Разработчики интерфейсов Наталья Стусь и Алексей Андросов вспомнили, как их команда прошла несколько итераций и построила тестирование фронтенда в Авто.ру на базе Jest и Puppeteer: юнит-тесты, тесты на отдельные React-компоненты, интеграционные тесты. Самое интересное из этого опыта — изолированное тестирование React-компонентов в браузере без Selenium Grid, Java и прочего.

Это сайт по продаже машинок. Алексей:
— Для начала надо немного рассказать, что такое Авто.ру. Авто.ру — очень большой проект, очень много кода. Там есть поиск, личный кабинет, автосервисы, запчасти, отзывы, кабинеты дилеров и многое другое. Одни и те же люди делают схожие задачи, например, для мобильных и десктопа. Весь код мы пишем в большой монорепе, потому что это все перемешивается. Вопрос — как ее тестировать? Получается много кода, и монорепа нам жизненно необходима.

Остались и небольшие кусочки на БЭМ. У нас есть React и Node.js, который выполняет рендеринг на стороне сервера и запрашивает данные из бэкенда.

Релизный цикл наших отдельных приложений включал в себя несколько шагов. Наталья:
— Мы начали думать в сторону автоматизации. После этого в этой же отдельной ветке фича тестируется ручными тестировщиками. Сначала фича разрабатывается программистом в отдельной ветке. Если нет, то снова возвращаемся на итерацию разработки, снова тестируем. Если все хорошо, задача попадает в релиз-кандидат. Пока тестировщик не скажет, что в этой фиче всё ок, она не попадет в релиз-кандидат.

Например, если мы собираемся катить декстопный веб, то идет ручной регресс десктопного веба. После сборки релиза-кандидата идет ручной регресс — не всего Авто.ру, а только того пакета, который мы собираемся катить. Такой регресс занимал примерно один рабочий день одного ручного тестировщика. Это очень много тест-кейсов, которые проводятся вручную.

После этого релизная ветка вливается в мастер. Когда регресс пройден, происходит релиз. Это проверяется не сразу, а только на следующем ручном регрессе — мобильного веба. На этом моменте мы как раз можем влить мастер-код, который мы протестировали только для десктопного веба, и этот код может сломать мобильный веб, например.

Все ручные тестировщики, естественно, устали проходить каждый день одно и то же. Естественно, самым больным местом в этом процессе был ручной регресс, который проводился очень долго. Первое решение, которое было выполнено, — автотесты Selenium и Java, написанные отдельной командой. Поэтому мы решили все автоматизировать. Написали около 5 тысяч таких тестов. Это были end-to-end-тесты, e2e, которые тестировали все приложение целиком. Чего мы в итоге добились?

Автотесты проходят гораздо быстрее, чем это делает ручной тестировщик, где-то раз в 10 быстрее получилось. Естественно, мы ускорили регресс. Найденные баги из автотестов проще воспроизводить. Соответственно, с ручных тестировщиков сняли рутинные действия, которые они выполняли каждый день. Просто перезапускаешь этот тест или смотришь по шагам, что он делает — в отличие от ручного тестировщика, который скажет: «Я что-то нажал, и все сломалось».

У нас всегда запускается один и тот же run тестов — в отличие, опять же, от ручного тестирования, когда тестировщик может посчитать, что вот это место мы не трогали, и я его в этот раз проверять не буду. Обеспечили стабильность покрытия. Всё благодаря скриншотным тестам. Мы добавили тесты на сравнение скриншотов, повысили точность тестирования UI — теперь проверяем расхождение в пару пикселей, которое тестировщик глазами не увидит.

Самой большой — для e2e-тестов нам нужно тестовое окружение, которое полностью соответствует проду. Но были и минусы. Для этого нужно практически столько же сил, сколько на поддержку стабильности прода. Его нужно всегда поддерживать в актуальном и рабочем состоянии. Поэтому у нас часто были ситуации, когда тестовое окружение лежит или где-то что-то сломано, и у нас падают тесты — хотя в самом фронтовом пакете проблем не было. Естественно, мы не всегда можем себе это позволить.

Они не могут сразу после релиза новой фичи прийти и тут же написать на нее автотесты. Еще эти тесты разрабатывает отдельная команда, у которой свои задачи, своя очередь в трекере задач, и новые фичи покрываются с некоторой задержкой. При этом отдельная команда нужна, и у нее будут отдельные инструменты, отдельная инфраструктура, все свое. Поскольку тесты дорогие и их сложно писать и поддерживать, мы покрываем ими не все сценарии, а только самые критичные. Покажу пару примеров. И разбор упавших тестов — тоже нетривиальная задача для ручных тестировщиков или для разработчиков.

500 тестов прошло, из них сколько-то упало. У нас есть run-тесты. Тут тест просто не запустился, и не понятно, хорошо там всё или нет. Мы в отчете можем увидеть вот такую штуку.

Он не смог найти какой-то элемент на странице, но почему — мы не знаем. Еще один пример — тест запустился, но упал вот с такой ошибкой. Это все нужно идти и дебажить руками. Либо этот элемент просто не появился, либо оказался не на той странице, либо поменялся локатор.

Здесь мы загружаем какую-то карту, она чуть-чуть сдвинулась, у нас тест упал. Скриншотные тесты тоже не всегда дают нам хорошую точность.

Часть тестов мы стали запускать на проде — те, которые не задевают юзерских данных, не меняют ничего в базе. Мы попытались решить ряд этих проблем. Мы просто ставим новый пакет фронтенда и прогоняем там тесты. То есть мы на проде сделали отдельную машинку, которая смотрит в прод-окружение. Прод, по крайней мере, стабильный.

Для этого был написан специальный сервис, который называется mockritsa, он помогает довольно легко сделать нужные моки для фронтенда и довольно легко в них проксироваться. Часть тестов мы перевели на моки, но у нас очень много разных бэкендов, разных API, и все это замокать — очень тяжелая задача, тем более для 5 тысяч тестов.

Даже после того, как мы попробовали решить эти проблемы, мы все равно пришли к выводу, что для CI нам такие тесты не подходят, они идут очень долго. Также нам пришлось купить кучу железа, чтобы наш Selenium grid устройств, с которых запускаются эти тесты, был больше, чтобы они не падали, потому что не смогли поднять браузер, и, соответственно, проходили побыстрее. Мы просто никогда в жизни потом не разберем эти отчеты, которые будут генериться на каждый пул-реквест. Мы не можем запускать их на каждый пул-реквест.

Мы хотим запускать тесты на пул-реквест без всяких тестовых стендов, бэкендов, баз данных, без каких-то сложных юзерских кейсов. Соответственно, для CI нам нужны быстрые и стабильные тесты, которые не будут падать по каким-то рандомным причинам.

Мы хотим, чтобы эти тесты писались одновременно с кодом, и чтобы по результатам тестов было сразу понятно, в каком файле что-то пошло не так.

Почему мы выбрали Jest? Алексей:
— Да, и мы решили попробовать все, что мы хотим, прямо все от начала и до конца поднять в одной и той же инфраструктуре Jest. Это популярный, поддерживаемый инструмент, у него там уже есть куча готовых интеграций: React test render, Enzyme. Мы уже писали юнит-тесты на Jest, он нам нравился. Все работает из коробки, ничего строить не надо, все просто.

В моке это делается на раз-два, а в Jest это сложно сделать: он постоянно запускается в отдельных потоках. И Jest лично для меня выиграл тем, что в отличие от какой-нибудь моки, в нем сложно выстрелить себе в ногу сайд-эффектом от какого-то стороннего теста, если я его забыл почистить или что-нибудь еще. И для e2e выпустили Puppeteer, мы его тоже решили попробовать. Можно, но сложно. Вот что у нас получилось.

Когда мы пишем тесты просто на какую-то функцию, тут особых проблем нет. Наталья:
— Тоже начну с примера модульных тестов. Мы вызываем эту функцию, передаем какие-то аргументы, сравниваем то, что получилось, с тем, что должно было получиться.

Нам нужно их как-то рендерить. Если речь идет про React-компоненты, то тут все становится чуть сложнее. Он будет рендерить компонент полностью до конца, до верстки. Есть React test renderer, но он не очень удобен для модульных тестов, потому что он не позволит нам протестировать компоненты изолированно.

Он получает какие-то пропы, в нем есть какая-то логика. И я хочу показать, как можно с помощью Enzyme написать модульные тесты на React-компоненты на примере такого компонента, где у нас есть некий MyComponent. Дальше он возвращает компонент Foo, который, в свою очередь, будет возвращать компонент bar, который уже в компонент bar возвращает нам, собственно говоря, верстку.

Это как раз то, что нам нужно, чтобы протестировать компонент MyComponent изолированно. Мы можем использовать такой инструмент из Enzyme, как shallow rendering. Мы просто будем тестировать логику именно компонента MyComponent. И эти тесты не будут зависеть от того, что внутри себя будут содержать компоненты foo и bar.

«Expect что-либо toMatchSnapshot» создаст нам такую структуру, просто текстовый файл, в котором хранится, собственно, то, что мы передали в expect, то, что получается, и при первом запуске таких тестов пишется вот этот файл. В Jest есть такая штука, как Snapshot, и они здесь тоже могут нам помочь. При дальнейших запусках тестов то, что получается, будет сравниваться с эталоном, который содержится в файле MyComponent.test.js.snap.

Мы можем написать такие два теста для наших двух случаев, для наших двух кейсов на компонент MyComponent. Здесь мы видим как раз, что весь рендеринг, он возвращает нам ровно то, что возвращает метод рендер из MyComponent, и то, что такое foo, ему, в общем-то, пофигу.

Но у этого подхода есть один минус. В принципе, то же самое мы можем протестировать без Snapshot, просто проверяя нужные нам сценарии, например, проверяя, какие пропы передаются в компонент foo. Если мы в MyComponent добавим какой-то еще элемент, наш новый тест, это никак не отображает.

Но если мы оба теста напишем на Snapshot, а потом сделаем такие же изменения в компоненте, то мы увидим, что у нас упадут оба теста. Поэтому, все-таки Snapshot-тесты — это те, которые покажут нам практически любые изменения внутри компонента. В принципе, результаты этих упавших тестов будут говорить нам об одном и том же, что у нас там добавился какой-то «привет».

Всю остальную логику проверять как-то иначе, без Snapshot, потому что Snapshot, они не очень показательны. И это тоже избыточно, поэтому, я считаю, что лучше использовать один Snapshot-тест на одну и ту же структуру. Это совершенно не подойдет для TDD, если вы хотите это использовать. Когда ты видишь Snapshot, просто видишь, что что-то отрендеилось, но не понятно, какую именно логику ты здесь тестировала. То есть потом, когда пойдете смотреть на этот компонент, вы увидите, что да, Snapshot соответствует чему-то, но какая там была логика, не очень понятно. И не пойдет, как документация.

Точно так же мы напишем модульные тесты на компонент foo, на компонент bar, допустим, Snapshot.

Мы считаем, что мы все проверили, мы молодцы. У нас получится стопроцентный coverage для этих трех компонент.

Тест мы поправили, и все три теста у нас проходят. Но, допустим, мы поменяли что-то в компоненте bar, добавили ему какой-то новый prop, и у нас упал тест на компонент bar, очевидно.

Мы не передаем, на самом деле, в компонент bar те prop, которые он ожидает. Но на самом деле если мы соберем всю эту историю, то работать ничего не будет, потому что MyComponent не соберется вот с такой ошибкой. Поэтому мы говорим о том, что в этом случае нужны еще интеграционные тесты, которые будут проверять, в том числе, правильно ли мы вызываем из своего компонента его дочерний компонент.

Имея такие компоненты и поменяв один из них, вы сразу видите, на что изменения в этом компоненте повлияли.

Shallow rendering сам по себе возвращает вот такую структуру. Какие возможности у нас есть в Enzyme для того, чтобы проводить интеграционное тестирование? Соответственно, вызывая его на компоненте foo, мы получаем то, что рендерит компонент foo, это bar, если мы сделаем dive еще раз, мы получим, собственно, уже ту верстку, которую нам возвращает компонент bar. В нем есть метод dive, если он вызван на каком-то React-компоненте, он провалится в него. Это как раз будет уже интеграционный тест.

Но я не советую это делать, потому что это будет очень тяжелый Snapshot. Либо можно отрендерить сразу все с помощью метода mount, который осуществляет full DOM rendering. Вам нужно только проверить интеграцию между родительским и дочерним компонентом в каждом случае. И, как правило, вам не нужно проверять полностью всю структуру.

То же самое, интеграционный тест мы добавим для компонента foo, что он правильно вызывает компонент bar, и тогда мы проверим всю эту цепочку, и будем уверены, что никакие изменения нам не сломают, собственно, рендеринг MyComponent И для MyComponent мы добавляем интеграционный тест, таким образом, в первом тесте я добавляю просто dive, и получается, что мы протестировали не только логику самого компонента, но и интеграцию его с компонентом foo.

Просто кратко про то, что еще умеют Jest и Enzyme. Еще один пример, уже из реального проекта. Вы можете, если используете какую-то внешнюю функцию в своем компоненте, можете ее замокать. Jest умеет моки. На самом деле, мок-функция. Например, в этом примере мы вызываем какой-то api, мы не хотим, естественно, в модульных тест ни в какое api ходить, поэтому мы просто мокаем функцию geiResource неким объектом jest.fn. Это все позволяет делать Jest. Потом мы сможем проверить, была ли она вызвана или не была, сколько раз она была вызвана, с какими аргументами.

Если вам нужен store, вы просто можете его передать туда, и он будет работать. В shallow-рендеринге в компонент можно передать store.

Также в уже отрендеренном компоненте можно поменять State и prop.

Он просто вызовет обработчик. Можно вызвать на каком-то компоненте метод simulate. Все это можно прочитать, естественно, в документации на Enzyme, очень много полезных штук. Например, если делать simulate click, вызовет onClick для компонента button вот здесь. Это просто несколько примеров из реального проекта.

Мы умеем проверять Jest, умеем писать юнит-тесты, проверять компоненты, проверять, какие элементы неправильно реагируют на клик. Алексей:
— Мы приходим к самому интересному вопросу. Теперь нам надо проверить верстку компонента, css. Умеем проверять у них html.

Если я проверяю html, то я вызвал shallow rendering, он мне взял и отрендерил html. И желательно это сделать так, чтобы принцип тестирования никак не отличался от того, который я описал раньше. Я хочу проверить css, просто вызвать какой-то рендер и просто проверить — ничего не поднимая, никаких инструментов не настраивая.

Вы открывает какую-то вкладку, вы переходите на какой-то page html, делаете скриншот и сравниваете его с предыдущим вариантом. Я начал это искать, и примерно везде давался один и тот же ответ на всю эту штуку, которая называется Puppeteer, или Selenium grid. Если не изменился, значит, все хорошо.

Желательно — в разных состояниях. Вопрос, что такое page html, если я просто хочу один компонент изолированно проверить?

Ссылка со слайда

Я не хочу писать кучу этих page html на каждый компонент, на каждое состояние. У Авито есть хороший заход. Рома Дворнов опубликовал статью на Хабре, и выступление у него, кстати, было. Они что делали? Они берут компоненты, через стандартный рендер собирают html. Потом с помощью плагинов и всяких хитростей собирают все assets, которые у них есть, — картинки, css. Вставляют это все в html, и у них как раз таки получается нужный html.

Ссылка со слайда

И потом они подняли специальный сервер, отправляют html туда, он его рендерит, и возвращает какой-то результат. Очень интересная статья, почитайте, правда, можно почерпнуть много интересных идей оттуда.

Сборка компонента отличается от того, как оно поедет в production. Что мне там не нравится. Я не могу гарантировать, что я протестировал то, что сейчас покачу. У нас, например, webpack, а там собирается каким-то babel assets, там вытаскивается по-другому.

Я хочу как-то попроще это сделать. И опять же, отдельный сервис для скриншотов. И попробуем использовать что-то, типа Dockerа, потому что это такая штука, ее можно поставить на комп, локально, это будет просто, изолированно, ничего не трогает, все хорошо. И была, собственно, идея, что, давайте мы будем собирать это ровно так же, как мы будем это собирать.

И родилась идея. Но эта проблема с page html, она остается все равно, что это такое, на самом деле. Описаны модули, как их собирать, выходной файл, все плагины у вас описаны, все настроено, все хорошо. Есть у вас такой упрощенный webpack.conf, и от него есть какой-то EntryPoint для клиентского js.

Он мне в мой компонент зайдет и соберет его изолированно. А что если я сделаю вот так? Если я еще добавлю туда html webpack, то она выход мне даст еще и html, и там будут собраны вот эти ассеты, и эту штуку, правда, уже можно тестировать автоматически. И там будет ровно один компонент.

И я уже собрался все это писать, но потом нашел вот это.

И я в него начал активно контрибьютить. Jest-puppeteer-React, молодой плагинчик. Проект, на самом деле, не мой. Если вдруг захотите его попробовать, то можете, например, ко мне подойти, я как-то могу помочь.

По сути, берешь конфиг webpack. Вы пишите обычный файл как test.js, а эти файлы нужно писать чуть отдельно, чтобы помочь их найти, чтобы не компилить вам весь проект, а скомпилить только нужные компоненты. И входные точки меняются на эти файлы browser.js, то есть, таким образом, собирается ровно то, что мы хотим протестировать, запакует все в html, и с помощью Puppeteer сделает вам скриншоты.

Он умеет снимать скриншоты, он умеет их сохранять с помощью другого плагина jest-image-snapshot. Что он умеет? В нем можно делать все, что можно делать в браузере, исполнять js, можно протестировать media-query, например, прямо сразу. И сравнивать их умеет.

Обычной web-консолью, и там прямо сидишь, дебажишь, понимаешь, что не так. Если не нравится headless-режим, там сложно, мы не можем отладить, не понимаем, в чем проблема, отключаем headless-режим, и вам открывается обычный Chrome с обычным дебагером.

У вас есть заранее подготовленный уже образ с этим хромом. И эта штука умеет запускаться в Docker. Ничего ставить, кроме Docker, не надо. Он настроен. И Docker нам еще решает такую проблему, что если вы один и тот же скриншот снимете, например, на маке и на Linux, он будет чуть-чуть на полпикселя отличаться, потому что шрифты там рендерятся чуть-чуть не так. Просто есть образ. А Docker эту проблему решает, потому что он всегда будет запускаться в одном и том же окружении.

Она не идеальна, но я прямо хочу это все доделать. Что эта штука пока не умеет? Там пока нет before-after, но это все можно эмулировать, не проблема. Несложно, скоро будет. Если дальше смотреть, я хочу запускать эту штуку для любой версии Chrome, а в идеале еще втащить туда Firefox. Пока нет моков, их тоже можно сделать. Это тоже реально сделать.

Сейчас там используется pixelmatch. И попробуем поиграться с разными сравнивалками скриншотов. Я пока попробовал, и вроде бы работает и правда быстрее и лучше. Но кажется, наша библиотека looksame, которая используется в «Гермионе», могла бы работать быстрее.

Если вы вспомните, это то, чего я хотел в идеале. Дальше — вот такой пример. То есть я могу использовать Redux и замокать store ему. В принципе, все так и получилось: у меня есть какой-то рендер, я просто из библиотеки беру другой рендер и отправляю туда обычный компонент — прямо как в Enzyme. Я могу задать viewport, могу указать, что я хочу себе сделать ретину. Любые компоненты. Потом просто сравниваю, смотрю, что получилось с эталонным скриншотом.

Есть эталон, есть результат теста. Вот как это все может быть. На глаз вообще не заметно, а на самом деле отличаются. Они отличаются?

Selenium такого никогда не сделает. Еще один плюс: я могу на один компонент написать 5-10 тестов и проверить их абсолютно в любом состоянии. На самом деле можно все сделать гораздо быстрее. Ему надо проект собрать, страницу загрузить, придумать, как сэмулировать это состояние именно в этом компоненте на этой странице.

То есть мы можем собрать проект, пойти прямо на тестовый сервер и своими руками написать e2e-тесты — ровно такие же, как были на Selenium. И поскольку мы уже втащили Puppeteer, бонусом мы получили e2e-тесты.

Мы пишем тесты на родном-любимом JS и на Puppeteer, который мы уже знаем, потому что пользуемся им в наших тестах на верстку. Наталья:
— Получается, что здесь мы не пишем тесты не на Selenium Java инструментами, которыми мы не владеем.

Еще у меня был пример, когда я просто в первый раз пробовала и то и другое, и написала два теста. Это пример из реального проекта, который я писала для нашего личного кабинета.

Выглядит чуть аккуратнее. Верхний большой — на Selenium и Java, нижний маленький — на JS Puppeteer. А здесь это все очень красиво и намного более читаемо, на мой взгляд, чем тест на Java. Там еще 18 строк импортов. Поэтому кажется, что проще будет научить разработчиков писать фронтенд и поддерживать такие тесты, чем заставить их писать тесты на Java и Selenium.

Наши разработчики теперь могут сами все сделать. Алексей:
— Что мы в итоге получили? И вдобавок e2e. Мы можем покрыть логику, html-верстку, можем быстренько покрыть css в любом состоянии. Кроме того, наши тесты лежат рядом с нашим кодом. Прекрасно.

Я могу что угодно наворотить и просто проверить. Разработчики уже по факту знают, что этот компонент протестирован, покрыт тестами. Тесты начинают работать как документация на этот компонент, потому что там много логики. Или, если тестов нет, — дописать их, например. Где-то она может быть скрытой, неявной, а тут тесты тебе все рассказывают: если ты передал вот так, то должно быть вот так.

Мы запускаем тесты на git hook, на пул-реквест, получая очень быстрый фидбек для разработчика. Поскольку инструмент один, и мы работаем в одних и тех же терминах, разработчику удобно все это писать. Спасибо. И мы приближаем green master — у нас монорепа, он нам нужен, чтобы удостоверяться, что один проект не ломает другой.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть