Русский
Русский
English
Статистика
Реклама

Admitad

Тестирование с использованием Puppeteer

27.01.2021 16:20:11 | Автор: admin


Любите ли вы тесты, как люблю их я: всеми фибрами души, со всей страстью и энтузиазмом, на которые только способен разработчик, жадный до полного покрытия кода?


В этой статье я расскажу о тестировании кода с помощью Puppeteer сервиса, который позволяет проверять работу скриптов в их естественной среде обитания в браузере. Это не полноценный туториал по Puppeteer, а скорее набор советов о том, как писать осмысленные и стабильно работающие тесты.


Но сначала немного о том, для чего использую Puppeteer я.


Я работаю в команде, которая занимается разработкой трекинговых решений, и неотъемлемой частью моей работы является покрытие новых функций тестами. Тесты бывают разные: от простых unit-тестов до масштабного интеграционного тестирования. От вида тестов зависит и окружение, в котором они будут запускаться.


Зачем вообще писать тесты?


Каждый раз, когда я пишу тесты, они помогают обнаружить баги. Но у тестирования есть и другие плюсы.


Один из них обнаружение подводных камней. Подготовка тест-кейсов позволяет сложить в голове целостную картину бизнес-логики и того, как она должна работать. Тесты заставляют еще раз всё обдумать, и, быть может, это-то и спасет вас от неожиданных проблем на проде.


Кроме того, хорошие тесты это вторая документация (а если ее нет, то единственная). По ним можно понять, какое поведение ожидается от кода, а не как оно достигнуто. Поэтому не стоит усложнять тесты и нагромождать их utils-функциями (а тем более писать тесты на тесты).


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


Тестирование с использованием Puppeteer


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


Весь код, связанный с этой статьей, доступен в репозитории puppeteer-showcase.


Запуск тестов


В ходе работы скриптов и тестов могут появляться побочные эффекты: записываться cookie, добавляться записи в localStorage и т. д., поэтому каждый тест надо запускать с чистого листа в новом окне браузера.


В этих тестах используется фреймворк Mocha

describe('Puppeteer test cases', () => {  // Перед каждым тестом запускаем браузер и переходим на тестовую страничку (127.0.0.1:5000)  beforeEach(async () => {    this.browser = await puppeteer.launch({        // Для тестов не обязательно видеть UI браузера, поэтому запускаем его в headless режиме.        headless: true,      });    this.page = await this.browser.newPage();    await this.page.goto('http://127.0.0.1:5000/');  });  // Не забываем закрывать открытый браузер  afterEach(async () => {    await this.browser.close();  });  // Наши тесты будут здесь });

Это достаточно очевидный, но очень важный момент: все написанные тесты должны быть независимыми и работать без опоры друг на друга. Писать связанные тесты плохо!


Вы ещё используете timeout? Тогда мы идём к вам


Предположим, у нас есть кнопка (#button1), при нажатии на которую выполняется следующий код:


let redirectUrl = 'https://example.com/default/url/';try {    const response = await fetch('https://example.com/api/some/endpoint/?with=params');    redirectUrl = await response.json();} catch (exc) {    console.log(exc);}window.location = redirectUrl;

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


В голову приходит несколько сценариев для тестирования:


  • положительный API повел себя как ожидалось и вернул redirectUrl;
  • отрицательный API недоступен, запрос не завершился;
  • отрицательный API вернул неверный ответ (например, не JSON).

Опишем их:


Тесты под катом
describe('button1 test cases', () => {  it('should follow returned redirectUrl if response is ok', async () => {    this.page.on('request', (request) => {      if (request.url().endsWith('/api/some/endpoint/?with=params')) {        request.respond({          status: 200,          contentType: 'application/json',          body: JSON.stringify('https://example.com/returned/redirect/url/'),        });      } else {        request.continue();      }    });    this.page.setRequestInterception(true);    this.page.click('#button1');    await new Promise(resolve => setTimeout(resolve, 100));    expect(this.page.url()).to.equal('https://example.com/returned/redirect/url/');  });  it('should follow default url if request is blocked', async () => {    this.page.on('request', (request) => {      if (request.url().endsWith('/api/some/endpoint/?with=params')) {        request.abort('blockedbyclient');      } else {        request.continue();      }    });    this.page.setRequestInterception(true);    this.page.click('#button1');    await new Promise(resolve => setTimeout(resolve, 100));    expect(this.page.url()).to.equal('https://example.com/default/url/');  });  it('should follow default url if request is invalid', async () => {    this.page.on('request', (request) => {      if (request.url().endsWith('/api/some/endpoint/?with=params')) {        request.respond({          status: 500,          contentType: 'text/html',          body: '<p>Error</p>',        });      } else {        request.continue();      }    });    this.page.setRequestInterception(true);    this.page.click('#button1');    await new Promise(resolve => setTimeout(resolve, 100));    expect(this.page.url()).to.equal('https://example.com/default/url/');  });});

В этих тестах используется библиотека Chai

Кода получилось много, как это часто бывает с тестами. Посмотрим внимательно на первый из них:


it('should follow returned redirectUrl if response is ok', async () => {  // Перехватываем все запросы со страницы  this.page.on('request', (request) => {    // Если это запрос к API, то возвращаем 200 ответ с redirectUrl в JSON    if (request.url().endsWith('/api/some/endpoint/?with=params')) {      request.respond({        status: 200,        contentType: 'application/json',        body: JSON.stringify('https://example.com/returned/redirect/url/'),      });    } else {      // Если это запрос не к API, то не пропускаем запрос      request.continue();    }  });  this.page.setRequestInterception(true);  // Кликаем по кнопке  this.page.click('#button1');  // Ждем 100 мс, пока пройдет запрос и сменится страница  await new Promise(resolve => setTimeout(resolve, 100));  // Проверяем, что мы попали на нужную страницу  expect(this.page.url()).to.equal('https://example.com/returned/redirect/url/');});

Пайплайн такой:


  1. нажимаем на кнопку;
  2. перехватываем запрос к API и возвращаем нужный redirectUrl;
  3. ждем 100 мс;
  4. проверяем, попали ли мы на нужную страницу.

Постойте, а что, если скрипт не успеет совершить запрос и перенаправить пользователя за 100 мс? Может быть, увеличить таймаут до 1 с? А что, если и за секунду не успеет? Да и если каждый тест будет ждать по секунде, сколько же будут работать все тесты?


Ответ прост: не используйте таймауты. У Puppeteer API есть множество методов, которые позволяют избежать таймаутов, дождавшись вместо этого совершения какого-либо действия. В нашем случае подойдет waitForNavigation. Этот метод ожидает, пока не произойдет смена страницы.


Хороший тест будет выглядеть так:


it('should follow returned redirectUrl if response is ok', async () => {  // Перехватываем все запросы со страницы  this.page.on('request', (request) => {    // Если это запрос к API, то возвращает 200 ответ с redirectUrl в JSON    if (request.url().endsWith('/api/some/endpoint/?with=params')) {      request.respond({        status: 200,        contentType: 'application/json',        body: JSON.stringify('https://example.com/returned/redirect/url/'),      });    } else {      // Если это запрос не к API, то не трогаем запрос      request.continue();    }  });  this.page.setRequestInterception(true);  // Кликаем по кнопке  this.page.click('#button1');  // Ждем, пока сменится страница  await this.page.waitForNavigation();  // Проверяем, что мы попали на нужную страницу  expect(this.page.url()).to.equal('https://example.com/returned/redirect/url/');});

И никаких таймаутов!




Это очень простой пример, но от того не менее показательный. Для того чтобы выйти победителем из битвы с таймаутами, необходимо продумывать очередность await-ов: сначала действие, затем его ожидание. Тогда и тесты ускорятся, и CI не будет падать, когда не хватит таймаута.


Среда выполнения тестов


Есть две независимые среды выполнения JavaScript-кода при написании тестов с Puppeteer:


  1. Node, при помощи которого были запущены тесты, в нем исполняется код тестов;
  2. браузер, который был открыт Puppeteer, в нем исполняется код, связанный с открытой страницей, бизнес-логика.

Эти две среды изолированы, у них нет общей области видимости. Однако при помощи API Puppeteer может общаться с браузером. Также с браузером взаимодействуют и тесты, в том числе выполняют код и получают его результат через evaluate.


Понимание того, что бизнес-логика и тесты для нее исполняются в двух разных изолированных средах, между которыми есть только один мостик в виде Puppeteer, является критичным.



Лучше картинки с котиком это может продемонстрировать только тест.


Тестируем console.log


На этот раз бизнес-логика будет выглядеть чуть проще:


console.log('Hello from main.js!');

и будет запускаться при нажатии на кнопку #button2.


Тогда напишем два теста:


  1. первый будет проверять, что при нажатии на кнопку в консоль окружения тестов ничего не пишется;
  2. второй что при нажатии на кнопку в консоль браузера пишется Hello from main.js!.

А вот и тесты:


describe('button2 test cases', () => {  it('should not print message to node console on button2 click', async () => {    const printedMessages = [];    // Подменяем console.log на функцию-шпиона и записываем логируемые сообщения    console.log = (message) => {      printedMessages.push(message);    }    // Нажимаем на кнопку, которая печатает в консоль    await this.page.click('#button2');    // Проверяем, что в консоль ничего не было написано    expect(printedMessages).to.be.empty;  });  it('should print message to browser console on button2 click', async () => {    // Подменяем браузерный console.log на функцию-шпиона и записываем логируемые сообщения    await this.page.evaluate(() => {      window.printedMessages = [];      window.console.log = (message) => {        window.printedMessages.push(message);      }    });    // Нажимаем на кнопку, которая печатает в консоль    await this.page.click('#button2');    // Извлекаем шпионские данные от подмененного console.log    const printedMessages = await this.page.evaluate(() => window.printedMessages);    // Проверяем, что в консоль было написано ожидаемое сообщение    expect(printedMessages).to.contain('Hello from main.js!');  });});

Пайплайн тут следующий:


  1. подменяем console.log (тестовый либо браузерный);
  2. нажимаем на кнопку 2;
  3. проверяем, какие сообщения были написаны через console.log.

Запускаем тесты и видим заветную картину:



should not print message to node console on button2 click и should print message to browser console on button2 click прошли, значит, я никого не обманул.


Подсматриваем за Puppeteer


Еще одна прелесть тестов с использованием Puppeteer их наглядность. Помните, как мы при запуске браузера указывали параметры?


this.browser = await puppeteer.launch({  headless: true,  // <-- параметр});

Мы использовали headless: true потому, что тесты так проходят (или падают) быстрее, так как не нужно тратить ресурсы на запуск графической оболочки браузера.


Однако если указать следующие параметры:


this.browser = await puppeteer.launch({  headless: false,  // <-- запускаем графическую оболочку  slowMo: 500,  // <-- включаем задержку между действиями в 500 мс});

и запустить тесты, мы сможем понаблюдать за тем, что делает Puppeteer по нашим указаниям:


загружаем...


Заключение


В этой статье я поделился небольшой частью интересностей Puppeteer, оставив за кадром его преимущества вроде кроссбраузерности (с некоторыми оговорками) и непростые задачи, которые приходилось решать для повышения стабильности тестов. Если мое повествование нашло отклик в вашей душе, напишите об этом в комментариях.


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


Спасибо за внимание!


исходный код


Подробнее..

Немного про трекинг и сервис переходов Admitad

14.12.2020 18:12:07 | Автор: admin

Привет, меня зовут Александр, я тимлид команды разработки трекинговых решений компании Admitad.


Почти всегда на собеседованиях мне задают вопросы, чем занимается команда, какие у нас проекты и т.д. На устное объяснение кандидатам принципа работы CPA-сети Admitad уходит достаточно много времени, к тому же это не очень наглядно. Поэтому я решил написать статью, где в виде ответов на вопросы собрана информация о трекинге, сервисах нашей команды и задачах, которые мы решаем. Также я расскажу, как у нас работает мониторинг, чем микросервисы лучше монолита, какая польза от QA и еще пару интересных вещей.


Чем занимается команда?


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


Что такое трекинг?


Трекинг это калька с английского слова tracking, которое переводится как отслеживание. Сразу поясню, что трекинг не означает шпионаж. В качестве всем знакомой аналогии можно привести трекинг почтовых отправлений: на почте посылку регистрируют и присваивают ей уникальный идентификатор, чтобы получатель в дальнейшем мог отслеживать статус ее доставки.



Аналогичным образом работает и трекинг партнерской сети Admitad, только Admitad отслеживает целевые действия пользователей для своих партнеров.


Какие сервисы задействованы в трекинге?


Наша команда разрабатывает и поддерживает сервисы переходов и регистрации действий Admitad. Вот так схематично можно представить их взаимодействие с Admitad и его партнерами (сплошными стрелками обозначены активные действия пользователей, пунктирными реакция сервисов на них).



Пояснения к схеме:


  • Партнерская ссылка Admitad инициирует редирект пользователя через сервис переходов.
  • Click_id, или идентификатор перехода, генерируется на лету и не содержит никаких персональных данных.
  • Примером сайта партнера может являться интернет-магазин, в котором пользователь хочет купить телевизор (совершить целевое действие).

Какие требования к сервису переходов?


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


Поскольку пользователи живут в разных странах, ноды сервиса переходов также закреплены за балансировщиками, размещенными в разных точках Земли. Благодаря этому среднее время ответа сервера составляет ~10 мс в любом регионе.


Рабочая нагрузка колеблется в пределах ~1 0002 000 RPS. Естественно, кластер имеет запас прочности на случай повышения нагрузки, так как объем трафика внезапно может вырасти в 1,52 раза.




Так как это основной сервис Admitad, его доступность 24/7 для пользователей обеспечивается резервированием нод в нескольких ДЦ. Идентификаторы click_id гарантированно доставляются в Admitad через очереди RabbitMQ.


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


Если в прод все-таки просочился баг или утекла память на сервере, уведомления в Sentry и Slack позволят вовремя среагировать на ошибку. А за текущими метриками железа мы наблюдаем на дашбордах Grafana.



Теперь, когда мы познакомились с сервисом переходов и требованиями к нему, расскажу подробнее про задачи команды на примере оптимизации сервиса.


Зачем делить сервис переходов?


Сервис переходов частично выполняет задачи по регистрации действий, так как у него удобная распределенная инфраструктура, и текущая схема user journey выглядит скорее так.



Функции двух различных по назначению сервисов совмещены в одном. Почему так получилось? Исторически технологическая платформа Admitad была монолитом. В процессе развития она начала распадаться на независимые сервисы. Нынешний сервис переходов один из них.


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



Как разогнать сервис переходов?


Объем трафика, проходящего через сервис переходов, ежегодно растет. В дни распродаж, такие как День холостяков 11.11 на AliExpress, достигается пиковая нагрузка, поэтому компания заранее готовится к ним: вводится code freeze, девопсы проверяют скрипты для развертывания дополнительных нод.


Очевидно, что горизонтальное и вертикальное масштабирование сервисов увеличивает расходы на поддержание инфраструктуры. Более предпочтительным вариантом является оптимизация веб-приложения, чтобы выдерживать больше RPS на том же железе, поэтому при написании нового сервиса было принято решение отказаться от Flask и перейти на aiohttp.


Мы (и не только мы) считаем, что асинхронный фреймворк позволит разогнать новый сервис, чтобы он держал большую нагрузку. Конечно, не стоит руководствоваться только предположениями, поэтому был запущен процесс нагрузочного тестирования. Мы завели выделенную ноду и задеплоили на нее приложение. Сейчас с помощью Apache JMeter и Locust нагружаются эндпоинты сервиса. Сравнив метрики старого и нового сервисов на одной и той же ноде, можно будет сделать обоснованные выводы о производительности.


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



Параллельно с разработкой мы занимаемся исследованиями legacy-кода и оптимизацией существующей кодовой базы. В ходе деления сервисов были найдены эндпоинты, которые когда-то, может, и были нужны, но никто уже не знает зачем. Поставили себе задачу разобраться. Убрали парочку сервисных классов, которые ничего больше не делают. Такие микрооптимизации приятный бонус к процессу разработки.


И как при этом все не сломать?


Как уже было сказано, сервис переходов является критически важным сервисом Admitad, поэтому при переносе логики в новый сервис нельзя допускать баги, которые приведут к безвозвратным потерям.


Мы попросили ребят из QA сделать нам E2E тесты, которые будут проверять сервис переходов как черный ящик. Подменив содержимое ящика, можно будет удостовериться, что поведение нового приложения для пользователя и Admitad осталось прежним. А подключение E2E тестов отдельным этапом в CI позволит нам в дальнейшем спокойно деплоиться по пятницам (на самом деле нет).



Что дальше?


У нас есть планы, как сделать сервис переходов лучше, быстрее и надежнее.


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

Послесловие


Спасибо, что прочитали этот обзор. Надеюсь, ответы на вопросы получились достаточно содержательными, чтобы вы получили общее представление о трекинговых сервисах Admitad.


Если у вас был опыт построения нагруженных сервисов на aiohttp или сравнения производительности разных Python-фреймворков, пожалуйста, поделитесь им в комментариях. Также было бы интересно почитать про опыт профилирования и нагрузочного тестирования распределенных систем. Какие инструменты использовали? Что посоветуете почитать по теме?

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru