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