Jest и Puppeteer: автоматизация тестирования веб-интерфейсов

https://www.valentinog.com/blog/ui-testing-jest-puppetteer/
  • Перевод
Эту статью написал программист из Италии Валентино Гаглиарди. Он говорит, что сразу после выхода Puppeteer его заинтересовала автоматизация тестирования веб-интерфейсов с использованием данной библиотеки и Jest. После этого он приступил к экспериментам.



Здесь речь пойдёт об основах работы с Puppeteer и Jest на примере тестирования веб-формы. Также тут будут рассмотрены особенности использования Chromium с пользовательским интерфейсом и без него, и некоторые полезные мелочи, касающиеся различных аспектов тестирования веб-страниц и организации рабочей среды. Валентино полагает, что, хотя Puppeteer — инструмент сравнительно новый и его API вполне может подвергаться изменениям, у него есть шанс занять достойное место в арсенале веб-разработчиков.

О некоторых особенностях рассматриваемых тестов


Недавно я писал тесты интерфейсов и в это время наткнулся на пост Кента С. Доддса, посвящённый повышению стабильности тестов за счёт использования атрибута data-*. Это, если в двух словах, пользовательские атрибуты, которые можно задавать для практически любых HTML-элементов. Они особенно полезны при организации обмена данными с JavaScript-программами.

Материал Кента попался мне очень вовремя, так как я тогда пользовался примерно такими конструкциями:

await page.waitForSelector("#contact-form");
await page.click("#name");
await page.type("#name", user.name);

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

Тестирование формы обратной связи


Итак, наша цель заключается в тестировании формы обратной связи на данной странице, работой над которой я занимаюсь. Вот эта форма:


Она включает в себя следующие элементы:

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

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

Настройка проекта


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


Jest — фреймворк для тестирования, разработанный Facebook. Jest даёт платформу для автоматизированного тестирования, а также базовую библиотеку, позволяющую строить утверждения (Expect).

Puppeteer —  библиотека для Node.js, которая позволяет управлять браузером Chromium без пользовательского интерфейса. Инструмент это довольно новый, поэтому самое время его опробовать и подумать над тем, нужен ли он в конкретном проекте, и если нужен — о том, как встроить его в существующую экосистему.

Faker — библиотека для Node.js, которая умеет генерировать случайные данные. Среди них — имена, телефоны, адреса. Это, кстати, нечто вроде Faker для PHP.

Если у вас уже есть проект, на котором вы хотите поэкспериментировать, установить необходимые библиотеки можно такой командой:

npm i jest puppeteer faker --save-dev

Установка Puppeteer займёт некоторое время, так как, кроме прочего, в ходе установки библиотеки устанавливается и браузер Chromium.

Chromium — это веб-браузер с открытым исходным кодом, который является основой Google Chrome. Chromium и Chrome имеют практически одинаковые возможности, основные отличия заключаются в особенностях лицензирования.

После того, как всё необходимое будет установлено, настроим Jest в package.json. Команда test должна указывать на исполняемый файл Jest:

"scripts": {
  "test": "jest"
}

Кроме того, в Jest я предпочитаю пользоваться такой конструкцией:

import puppeteer from "puppeteer";

Поэтому тут нам понадобится Babel для Jest:

npm i babel-core babel-jest babel-preset-env --save-dev

После установки Babel создадим в папке проекта файл .babelrc со следующим содержимым:

{
  "presets": ["env"]
}

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

Пишем тесты


Создадим новую директорию в папке проекта. Назвать её можно test или spec. Затем, в этой директории, надо создать файл form.spec.js.

Теперь предлагаю рассмотреть код тестов по частям, начав с секции импорта. Ниже я приведу весь этот код целиком.

Сначала импортируем Faker и Puppeteer:

import faker from "faker";
import puppeteer from "puppeteer";

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

const APP = "https://www.change-this-to-your-website.com/contact-form.html"

Теперь, с помощью Faker, создаём фиктивного пользователя:

const lead = {
  name: faker.name.firstName(),
  email: faker.internet.email(),
  phone: faker.phone.phoneNumber(),
  message: faker.random.words()
};

Дальше — определяем некоторые переменные, необходимые для работы с Puppeteer:

let page;
let browser;
const width = 1920;
const height = 1080;

Теперь настраиваем поведение Puppeteer:

beforeAll(async () => {
  browser = await puppeteer.launch({
    headless: false,
    slowMo: 80,
    args: [`--window-size=${width},${height}`]
  });
  page = await browser.newPage();
  await page.setViewport({ width, height });
});
afterAll(() => {
  browser.close();
});

Здесь мы пользуемся методами Jest beforeAll и afterAll. Первый нам нужен из-за того, что перед выполнением тестов требуется запустить, с помощью Puppeteer, браузер. После запуска браузера мы можем открыть новую страницу. Когда тесты завершатся, браузер должен быть закрыт. Делается это в методе afterAll с помощью команды browser.close().

Надо отметить, что мы не ограничены лишь методами beforeAll и afterAll. Для того, чтобы узнать о других возможностях Jest, взгляните на документацию к этой библиотеке. В любом случае, рекомендуется пользоваться одним экземпляром браузера для выполнения всего набора тестов, вместо того, чтобы открывать и закрывать браузер для каждого отдельного теста.

Тут мне хотелось бы сделать некоторые комментарии по поводу вышеприведённого фрагмента кода. А именно, обратите внимание на то, что я запускаю браузер в оконном режиме, используя параметр headless: false. В данном случае так сделано для того, чтобы иметь возможность записать происходящее на экране на видео и показать процесс тестирования. Выполняя реальные тесты с помощью описываемых инструментов обычно незачем наблюдать за тем, что происходит. Для того, чтобы браузер запускался без интерфейса, можно просто убрать параметры, используемые при вызове метода launch().

То же самое касается и команды setViewPort(), которую тоже можно убрать. Или, что даже лучше, можно настроить два разных окружения тестирования. Одно использовать для визуальной отладки (речь об этом пойдёт ниже), второе — для работы с браузером без пользовательского интерфейса.

Теперь пишем код тестов:

describe("Contact form", () => {
  test("lead can submit a contact request", async () => {
    await page.waitForSelector("[data-test=contact-form]");
    await page.click("input[name=name]");
    await page.type("input[name=name]", lead.name);
    await page.click("input[name=email]");
    await page.type("input[name=email]", lead.email);
    await page.click("input[name=tel]");
    await page.type("input[name=tel]", lead.phone);
    await page.click("textarea[name=message]");
    await page.type("textarea[name=message]", lead.message);
    await page.click("input[type=checkbox]");
    await page.click("button[type=submit]");
    await page.waitForSelector(".modal");
  }, 16000);
});

Обратите внимание на возможность использования конструкции async/await с Jest. Тут предполагается, что тестирование проводится с использованием одной из свежих версий Node.js.

Рассмотрим эти тесты. Вот какие действия выполняет браузер, управляемый программно:

  • Переход по адресу, заданному в константе APP.
  • Ожидание появления формы обратной связи.
  • Щелчки по полям и заполнение их данными.
  • Установка флажка.
  • Отправка формы.
  • Ожидание появления модального окна.

Обратите внимание на то, что функции Jasmine test(), в качестве второго параметра, передан тайм-аут (16000). Это позволяет наблюдать за тем, как именно браузер работает со страницей.

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

Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL

Если выполнять тестирование, запуская браузер без интерфейса, тайм-аут можно убрать.

Теперь всё готово и тестирование можно запустить следующей командой:

npm test

После этого остаётся лишь наблюдать за браузером, который сам работает со страницей.


Если кому интересно, это экранное видео было записано в Fedora с помощью recordmydesktop и такой команды:

recordmydesktop --width 1024 --height 768 -x 450 -y 130 --no-sound

Однако, это ещё не всё.

Тестирование других элементов интерфейса


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

Выясним, как обстоят дела с тем, что находится в теге <title></title>. Как известно, там должен быть осмысленный заголовок страницы:

describe("Testing the frontend", () => {
  test("assert that <title> is correct", async () => {
    const title = await page.title();
    expect(title).toBe(
      "Gestione Server Dedicati | Full Managed | Assistenza Sistemistica"
    );
  });
  // Сюда можно добавить ещё тестов
});

А что у нас с навигационной панелью? Она должна присутствовать на странице. Проверим, с помощью Jest и Puppeteer, так ли это:

//
  test("assert that a div named navbar exists", async () => {
    const navbar = await page.$eval(".navbar", el => (el ? true : false));
    expect(navbar).toBe(true);
  });
//

Ещё можно выяснить, содержит ли некий элемент тот текст, который в нём должен быть:

//
  test("assert that main title contains the correct text", async () => {
    const mainTitleText = await page.$eval("[data-test=main-title]", el => el.textContent);
    expect(mainTitleText).toEqual("GESTIONE SERVER, Full Managed");
  });
//

А как насчёт испытаний страницы на предмет поисковой оптимизации? Проверим, например, наличие канонической ссылки:

describe("SEO", () => {
  test("canonical must be present", async () => {
    await page.goto(`${APP}`);
    const canonical = await page.$eval("link[rel=canonical]", el => el.href);
    expect(canonical).toEqual("https://www.servermanaged.it/");
  });
});

По тем же принципам можно создать множество других тестов.

В итоге все мои тесты успешно завершились, о чём можно судить по приятным сообщениям зелёного цвета.


Визуальная отладка


Мы уже говорили о том, что Puppeteer позволяет автоматизировать работу с Chromium, запускаемым с пользовательским интерфейсом или без него. Вспомним следующий фрагмент кода:

beforeAll(async () => {
  browser = await puppeteer.launch({
      // Режим отладки
      headless: false,
      slowMo: 80,
      args: [`--window-size=1920,1080`]
    });
  page = await browser.newPage();
///
});

Кроме того, нужно помнить о том, что запуская браузер с графическим интерфейсом, надо передать Jasmine параметр тайм-аута. В противном случае тестирование завершится быстро и неожиданно. Тайм-аут — это второй аргумент метода test():

describe("Contact form", () => {
  test(
    "lead can submit a contact request",
    async () => {
    ///// утверждения
    },
    16000 // <<< Тайм-аут Jasmine
  );
});

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

Решить эту задачу можно, создав вспомогательную функцию. Сделаем такую и поместим её в файл testingInit.js:

export const isDebugging = () => {
  let debugging_mode = {
    puppeteer: {
      headless: false,
      slowMo: 80,
      args: [`--window-size=1920,1080`]
    },
    jasmine: 16000
  };
  return process.env.NODE_ENV === "debug" ? debugging_mode : false;
};

Затем к ней можно обратиться из файла с кодом теста, сначала импортировав её, а потом воспользовавшись ей при запуске браузера:

///
import { isDebugging } from "./testingInit.js";
///
beforeAll(async () => {
  browser = await puppeteer.launch(isDebugging().puppeteer)); // <<< Визуальный режим
  page = await browser.newPage();
///
});

Та же функция пригодится и при настройке тайм-аута:

describe("Contact form", () => {
  test(
    "lead can submit a contact request",
    async () => {
    ///// утверждения
    }, isDebugging().jasmine // <<< тайм-аут Jasmine
  );
});

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

npm test

Для запуска тестов в визуальном режиме надо будет сделать следующее:

NODE_ENV=debug npm test

Итоги


Возможно, вам пока не вполне удобно работать с самими Puppeteer или его API. Я вас понимаю. И если новизна этого проекта наполняет вас сомнением в его практической применимости, вы можете взглянуть, например, на Cypress. Однако, Puppeteer даёт разработчикам поистине безграничные возможности. Сейчас создаются тестировочные фреймворки на основе этой библиотеки. Конечно, со временем API Puppeteer может и поменяться, но полагаю, что базовые вещи, о которых мы тут говорили, никуда не денутся. Кроме того, нельзя не отметить, что Puppeteer отлично сочетается с Jest. Многие, кроме того, выполняют с помощью Puppeteer E2E-тестирование.

Уважаемые читатели! Как вы автоматизируете тестирование веб-интерфейсов?
  • +20
  • 13,4k
  • 8
RUVDS.com 669,92
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией
Комментарии 8
    0

    Может быть есть какая-то реализация шаблона Page Object для Puppeteer?

      0

      Есть вот такой паттерн: https://medium.com/@boriscoder/page-object-for-testing-react-js-components-49cddb59adb7


      Он от фреймворка не зависит, с некоторыми изменениями можно и puppeteer обернуть.

        0

        UPD вот пример PageObject безо всяких библиотек для страницы из статьи


        function ContactForm(page) {
            this.init = () => page.waitForSelector("[data-test=contact-form]");
            this.fillField = async (name, value) => {
                const selector = `[name=${name}]`;
                await page.click(selector)
                await page.type(selector, value)
            };
            this.submit = () => page.click("button[type=submit]");
        }

        использование


        test('fill the form', async () => {
          const form = new ContactForm(page);
          await form.init();
        
          await form.fillField("name", lead.name);
          await form.fillField("email", lead.email);
          await form.submit();
        })

        писать простыни однотипных click,type, как в статье, однозначно не стоит.

        0
        В Яндексе для E2E тестирования используется Hermione. К данному инструменту также написаны некоторые удобные плагины.
          0
          Спасибо за статью! Достаточно много пользовался Selenium и различными фреймворками «вокруг его», но в итоге перешёл к подходу, в котором webpack в тестовую сборку пакует набор тестов, написанных на javascript. Набор тестов может быть запущен как вручную тестировщиком прямо с web-интерфейса, так и автоматическим параметром при настройке сборки (спец. сборка webpack'ом для jenkins, например).
          При автоматическом запуске, браузер стартовал c Node скрипта, который в итоге ожидал, что фронт пришлёт ему результаты прохождения тестов.
          Как раннер использовал mocha, но к нему особо привязан не был, можно использовать какой вам угодно.
          Для упрощения работы с интерфейсом чистым javascript'ом использовал вот такую поделку github.com/evegreen/useractions
          Из плюсов:
          • Я почти перестал писать явно ожидание каких-либо используемых мною далее элементов на странице, так как useractions автоматически перед любым действием ждал их (реализация похожа на FluentWait)
          • При сравнении с идентичными тестами на Selenium я получил прирост в скорости в 15 раз (в среднем) (и это при том, что в тестах на Selenium везде использовался FluentWait!)
          • Тесты работали абсолютно идентично в Chrome, Chrome для Android и Firefox без каких-либо танцев с бубном (уверен, что и в других актуальных браузерах всё заведётся так же).

          Из минусов (субъективно):
          • Тестировщикам пришлось писать тесты на JS, а это им далось сложнее, чем к примеру на Java / Python + Selenium
            0
            Когда тестировал интерфейсы использовали связку Selenium + Python (версии 3). К сложению сейчас наблюдаю проблему с совместимостью с браузерами, хотя и тогда она была (тесты проходившие на FF не всегда проходили на IE или Chrome). Возможно тестирование на js лучше с этим справляется.
              0
              пишу тесты фронта, используя codecept.io, довольно удобный и простой фреймворк (nodejs + webdriverio + mocha)
                0
                Ппц, гляжу форма знакомая, имя вроде где-то слышал смотрю — батюшки, да этоже создатель невообразимого вордпресс-говнеца которое я сейчас поддерживаю, от него клиент ко мне сбежал так он ещё наехал на меня типа моя VPS на Ovh медленная и небезопасная, коуч нашёлся, да.

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

                Самое читаемое