javascript

Тестирование компонентов с Puppeteer и Jest

  • среда, 17 января 2018 г. в 03:14:13
https://habrahabr.ru/post/346676/
  • Тестирование IT-систем
  • JavaScript



На Хабре есть публикация, описывающая написание тестов с использованием Puppeteer и Jest. Рекомендую к ознакомлению, если вы ещё не знаете, что такое Puppeteer. В данной статье, на примере React-компонента, будет описываться способ тестирования вызовов callback-функций. Например, есть компонент с props onChange, и необходимо протестировать, что при некоторых действия пользователя будет вызвана callback-функция с ожидаемыми переданными параметрами. Для этого будет использоваться библиотека Puppeteer-io. Но для начала рассмотрим небольшой примерчик на html и чистом javascript без привязки к библиотекам или фреймворкам…

Предположим, есть функция addEvent, которая вешает обработчик события на элементы по селектору. Нужно написать тест, который проследит, что обработчик вызывается по событию. Создадим файл index.html:

<button>Тестовая кнопка</button>
<script>

function addEvent(selector, eventType, handler) {
    let elements = document.querySelectorAll(selector);

    Array.prototype.forEach.call(elements, element => {
        element.addEventListener(eventType, handler, false);
    });
}

addEvent("button", "click", event => console.log("Button.click"));
</script>

Это страница, на которой происходит тестирование. На ней есть кнопка, на которую вешается обработчик события click. По этому событию, обработчик вызывает console.log, передавая строку-идентификатор действия, получение которой и будет означать, что тест пройден успешно. Теперь создадим файл index.html.test.js, в котором будет код теста для Jest:

const puppeteer = require("puppeteer");
const io = require("puppeteer-io");

test(`addEvent() корректно добавляет обработчики событий`, async () => {
    let browser = await puppeteer.launch();
    let page = await browser.newPage();

    await page.goto(`file://${__dirname}/test.html`);

    await io({
        page,
        async input() {
            await page.click("button");
        },
        async output({ message }) {
            await message("Button.click");
        }
    });

    await page.close();
    await browser.close();
});

Теперь подробнее про Puppeteer-io. Эта библиотека принимает две асинхронные функции, которые запускаются параллельно. В функции input() осуществляется управление браузером, например, клики по элементам или имитация ввода с клавиатуры, а в output(api) получаются данные из браузера и обрабатываются. В данном случае используется функция message, в которую передаётся строка-идентификатор ожидаемого сообщения. Если в браузере не будет вызван console.log с таким идентификатором, то тест подвиснет и Jest будет считать его провалившимся.

Тестируем React-компонент


В качестве примера будет использоваться такой компонент:

import React from "react";
import PropTypes from "prop-types";

const iItem = PropTypes.shape({
    id: PropTypes.string.isRequired,
    text: PropTypes.string.isRequired
});

export default class Select extends React.Component {
    static propTypes = {
        items: PropTypes.arrayOf(iItem).isRequired,
        onChange: PropTypes.func
    };

    getChangeHandler() {
        return ({ target }) => {
            if (this.props.onChange) {
                this.props.onChange(this.props.items[target.selectedIndex].id);
            }
        };
    }

    toOption(item, index) {
        return <option key={`id_${index}_${item.id}`}>
            {item.text}
        </option>
    }

    render() {
        return <select onChange={this.getChangeHandler()}>
            {this.props.items.map(this.toOption)}
        </select>
    }
}

Это просто обертка над стандартным select-ом. Когда в нем выбирают какой-нибудь пункт, вызывается callback-функция, в которую передается id выбранного пункта. Собственно эту функциональность мы и будем тестировать. Для этого создадим специальную страницу для тестирования, которую Puppeteer будет открывать в браузере:

import React from "react";
import ReactDOM from "react-dom";
import Select from "path-to/select-component.js";

const testItems = [
    { id: "0e210d4a-ccfd-4733-a179-8b51bda1a7a5", text: "text 1"},
    { id: "ea2cecbd-206c-4118-a1c9-8d88474e5a87", text: "text 2"},
    { id: "c812a9dc-6a54-409e-adb5-8eb09337e576", text: "text 3"}
];

// Передаем тестовые данные
console.log("test-items", testItems);

function TestPage() {
    const onChange = id => console.log("Select: change", id);
    
    return <div>
       <Select items={testItems} onChange={onChange} />
    </div>
}

ReactDOM.render(<TestPage />, document.getElementById("application"));

И развернем эту страницу, например, по url http://localhost:8080. Обратите внимание, что теперь в console.log передаются два аргумента: первый — id, а вторым аргументом передаются данные. Теперь напишем код теста:

const puppeteer = require("puppeteer");
const io = require("puppeteer-io");

test(`в onChange передается корректный id`, async () => {
    let browser = await puppeteer.launch();
    let page = await browser.newPage();

    await io({
        page,
        async input() {
            await page.goto("http://localhost:8080");

            let select = await page.$("select");

            await select.focus();
            await select.press("Enter");
            await select.press("ArrowDown");
            await select.press("Enter");
        },
        async output({ dataFromMessage }) {
            let [,secondItem] = await dataFromMessage("test-items");
            let selectedId = await dataFromMessage("Select: change");

            expect(selectedId).toBe(secondItem.id);
        }
    });

    await page.close();
    await browser.close();
});

Рассмотрим код функции output. Первым делом нужно получить тестовые данные. Для этого важно вызвать переход по url в input-потоке, ведь page.goto ожидает события onLoad страницы, а к тому моменту console.log("test-items", testItems) уже отработаете, и сообщение не будет получено. Для получения данных используется функция dataFromMessage, которая возвращает второй аргумент, передаваемый в console.log. Когда тестовые данные получены, можно ждать выбранный id, и сверять полученный результат с ожидаемым. Функциональность протестирована.

Отлов ошибок


В завершении, пример того, как можно обрабатывать ошибки. Создадим страничку:

<script>
    throw new Error("test-error");
</script>

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

const puppeteer = require("puppeteer");
const io = require("puppeteer-io");

test(`проверяет, что на странице произошла ошибка`, async () => {
    let browser = await puppeteer.launch();
    let page = await browser.newPage();

    await io({
        page,
        async input() {
            await page.goto(`file://${__dirname}/index.html`);
        },
        async output({ error }) {
            await error("test-error");
        }
    });

    await page.close();
    await browser.close();
});

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