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

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


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


В этой статье я расскажу о тестировании кода с помощью 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, оставив за кадром его преимущества вроде кроссбраузерности (с некоторыми оговорками) и непростые задачи, которые приходилось решать для повышения стабильности тестов. Если мое повествование нашло отклик в вашей душе, напишите об этом в комментариях.


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


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


исходный код


Источник: habr.com
К списку статей
Опубликовано: 27.01.2021 16:20:11
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании admitad

Javascript

Программирование

Тестирование веб-сервисов

Puppeteer

Unit testing

Admitad

Категории

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

  • Имя: Макс
    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-2023, personeltest.ru