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