вНовости программирования

Знакомство с фронтенд-тестированием. Часть четвертая. Интеграционное тестирование

Рассказывает Гил Тайяр, автор блога на Hackernoon


Мы рассмотрели два вида тестирования: юнит-тестирование различных модулей и E2E-тестирование всего приложения. Но между этими двумя этапами тестирования происходят и другие. Я, как и многие другие, называю такие тесты интеграционными.

Несколько слов о терминологии

Много общаясь с любителями разработки через тестирование, я пришёл к выводу, что они имеют другое определение для термина «интеграционные тесты». С их точки зрения, интеграционный тест проверяет «внешний» код, то есть тот, который взаимодействует с «внешним миром», миром приложения.

Поэтому, если их код использует Ajax или localStorage, или IndexedDB и, следовательно, не может быть протестирован с помощью юнит-тестов, они оборачивают этот функционал в интерфейс и мокают этот интерфейс для юнит-тестов, а тестирование реальной реализации интерфейса называют «интеграционным тестом». С этой точки зрения «интеграционный тест» просто тестирует код, который взаимодействует с «реальным миром» вне тех юнитов, которые работают без учета реального мира.

Я, как и многие другие, склонен использовать понятие «интеграционные тесты» для обозначения тестов, которые проверяют интеграцию двух или более юнитов (модулей, классов и т. д.). При этом неважно, скрываете ли вы реальный мир через замоканные интерфейсы.

Мое эмпирическое правило о том, следует ли использовать реальные реализации Ajax и других операций I/O (ввода-вывода) в интеграционных тестах, заключается в следующем: если вы можете это сделать и тесты все еще выполняются быстро и не ведут себя странно, то проверяйте I/O. Если операция I/O сложная, медленная или просто странная, то используйте в интеграционных тестах mock-объекты.

В нашем калькуляторе, к счастью, единственным реальным I/O является DOM. Нет вызовов Ajax и других причин писать «моки».

Фейковый DOM

Возникает вопрос: нужно ли писать фейковый DOM в интеграционных тестах? Применим моё правило. Использование реального DOM сделает тесты медленными? К сожалению, ответ — «да»: использование реального DOM означает использование реального браузера, что делает тесты медленными и непредсказуемыми.

Мы отделим большую часть кода от DOM или протестируем всё вместе в E2E-тестах? Оба варианта не оптимальны. К счастью, есть третье решение: jsdom. Этот замечательный и удивительный пакет делает именно то, чего от него ждёшь — реализует DOM в NodeJS.

Он работает, он быстр, он запускается в Node. Если вы используете этот инструмент, то можете перестать рассматривать DOM как «I/O». А это очень важно, ведь отделить DOM от фронтенд-кода сложно, если не невозможно. (Например, я не знаю, как сделать это.) Я предполагаю, что jsdom был написан именно для запуска фронтенд-тестов под Node.

Давайте посмотрим, как он работает. Как обычно, есть инициализирующий код и есть тестовый код, но на этот раз мы начнём с тестового. Но перед этим — отступление.

Отступление

Эта часть является единственной частью серии, которая ориентирована на конкретный фреймворк. И фреймворк, который я выбрал — это React. Не потому, что это лучший фреймворк. Я твердо верю, что нет такого понятия. Я даже не считаю, что существуют лучшие фреймворки для конкретных случаев использования. Единственное, во что я верю — люди должны использовать среду, в которой им наиболее комфортно работать.

И фреймворком, с которым мне наиболее комфортно работать, является React, поэтому следующий код написан на нём. Но, как мы увидим, интеграционные тесты фронтенда с использованием jsdom должны работать во всех современных фреймворках.

Вернемся к использованию jsdom.

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

const React = require('react') const e = React.createElement const ReactDom = require('react-dom') const CalculatorApp = require('../../lib/calculator-app') ... describe('calculator app component', function () { ... it('should work', function () { ReactDom.render(e(CalculatorApp), document.getElementById('container')) const displayElement = document.querySelector('.display') expect(displayElement.textContent).to.equal('0')

Интересными являются строки с 10 по 14. В строке 10 мы визуализируем компонент CalculatorApp, который (если вы следите за кодом в репозитории) также отображает компоненты Display и Keypad.

Затем мы проверяем, что в строках 12 и 14 элемент в DOM показывает на дисплее калькулятора начальное значение, равное 0.

И этот код, который работает под Node, использует document! Глобальная переменная document является переменной браузера, но вот она здесь, в NodeJS. Чтобы эти строки работали, требуется очень большой объем кода. Этот очень большой объем кода, который находится в jsdom, является, по сути, полной реализацией всего, что есть в браузере, за вычетом самого рендеринга!

Строка 10, которая вызывает ReactDom для визуализации компонента, также использует documentwindow), так как ReactDom часто использует их в своем коде.

Итак, кто создает эти глобальные переменные? Тест — давайте посмотрим на код:

before(function () { global.document = jsdom(`<!doctype html><html><body><div id="container"/></div></body></html>`) global.window = document.defaultView }) after(function () { delete global.window delete global.document })

В строке 3 мы создаём простой document, который содержит лишь div.

В строке 4 мы создаём глобальное window для объекта. Это нужно React.

Функция cleanup удалит эти глобальные переменные, и они не будут занимать память.

В идеале переменные document и window должны быть не глобальными. Иначе мы не сможем запустить тесты в параллельном режиме с другими интеграционными тестами, потому что все они будут переписывать глобальные переменные.

К сожалению, они должны быть глобальными — React и ReactDom нуждаются в том, чтобы document и window были именно такими, поскольку вы не можете им их передать.

Обработка событий

А как насчет остальной части теста? Давайте посмотрим:

ReactDom.render(e(CalculatorApp), document.getElementById('container')) const displayElement = document.querySelector('.display') expect(displayElement.textContent).to.equal('0') const digit4Element = document.querySelector('.digit-4') const digit2Element = document.querySelector('.digit-2') const operatorMultiply = document.querySelector('.operator-multiply') const operatorEquals = document.querySelector('.operator-equals') digit4Element.click() digit2Element.click() operatorMultiply.click() digit2Element.click() operatorEquals.click() expect(displayElement.textContent).to.equal('84')

Остальная часть теста проверяет сценарий, в котором пользователь нажимает «42 * 2 =» и должен получить «84».

И он делает это красивым способом — получает элементы, используя известную функцию querySelector, а затем использует click, чтобы щелкнуть по ним. Вы даже можете создать событие и иницировать его вручную, используя что-то вроде:

var ev = new Event("keyup", ...); document.dispatchEvent(ev);

Но встроенный метод click работает, поэтому мы используем его.

Так просто!

Проницательный заметит, что этот тест проверяет точно то же самое, что и E2E-тест. Это правда, но обратите внимание, что этот тест примерно в 10 раз быстрее и является синхронным по своей природе. Его гораздо проще писать и гораздо легче читать.

А почему, если тесты одинаковы, нужен интеграционный? Ну, просто потому, что это учебный проект, а не настоящий. Два компонента составляют всё приложение, поэтому интеграционные и E2E-тесты делают одно и то же. Но в реальном приложении E2E-тест состоит из сотен модулей, тогда как интеграционные тесты включают в себя несколько, быть может, 10 модулей. Таким образом, в реальном приложении будет около 10 E2E-тестов, но сотни интеграционных тестов.

Типичный программист.

Источник: Типичный программист