- Регистрация
- 9 Май 2015
- Сообщения
- 1,071
- Баллы
- 155
- Возраст
- 52
Рассказывает , автор блога на Hackernoon
В мы познакомились с юнит-тестированием: проверили основную логику приложения, содержащуюся в calculator, используя Mocha и .
В этой части мы рассмотрим сквозное (E2E) тестирование: протестируем всё приложение целиком, причём сделаем это с точки зрения пользователя, по сути, автоматизируя все его действия.
В нашем случае приложение состоит только из фронтенда — бэкенда попросту нет, поэтому E2E-тестирование будет заключаться в открытии приложения в реальном браузере, выполнении набора вычислений и проверке валидности значения на экране.
Нужно ли проверять все перестановки, как мы делали это в юнит-тестах? Нет, ведь это уже проверено! В E2E-тестах мы проверяем работоспособность не отдельных юнитов, а всей системы сразу.
Сколько нужно E2E-тестов?
Первая причина, по которой таких тестов не должно быть много, — хорошо написанных интеграционных и юнит-тестов должно хватить. E2E-тесты должны проверить, что все элементы корректно связаны между собой.
Вторая причина — они медленные. Если их будет сотня, как юнит-тестов и интеграционных, то тестирование будет проходить очень долго.
Третья причина — непредсказуемое поведение E2E-тестов. О таком явлении есть в блоге Google, посвященном тестированию. В юнит-тестах не наблюдается такого нестабильного поведения. Они могут то проходить, то падать — причем без видимых изменений, исключительно из-за I/O. Можно ли убрать непредсказуемость? Нет, но можно свести её к минимуму.
Чтобы избавиться от непредсказуемости, делайте как можно меньше E2E-тестов. Пишите один E2E-тест на десять других, и лишь тогда, когда они действительно необходимы.
Пишем E2E-тесты
Перейдём к написанию E2E-тестов. Нам нужны две вещи: браузер и сервер для нашего фронтенд-кода.
Для E2E-тестирования, как и для юнит-тестирования, мы будем использовать Mocha. Мы настроим браузер и веб-сервер, используя функцию before, и обнулим настройки при помощи функции after. Эти функции запускаются до и после выполнения всех тестов и настраивают окружение, которое могут использовать тестовые функции. Узнать о том, как они работают, можно в .
Сперва взглянем на настройку веб-сервера.
Настройка веб-сервера в Mocha
Веб-сервер на Node? На ум сразу же приходит , давайте посмотрим код:
let server
before((done) => {
const app = express()
app.use('/', express.static(path.resolve(__dirname, '../../dist')))
server = app.listen(8080, done)
})
after(() => {
server.close()
})
В функции before мы создаем express-приложение, указываем ему папку dist и прописываем слушать порт 8080. В функции after мы «убиваем» сервер.
Папка dist — это то место, где мы храним наши JS-скрипты и куда копируем HTML- и CSS-файлы. Вы можете увидеть, что мы делаем это в сборочном скрипте npm в package.json:
{
"name": "frontend-testing",
"scripts": {
"build": "webpack && cp public/* dist",
"test": "mocha 'test/**/test-*.js' && eslint test lib",
...
},
Это значит, что для E2E-тестов нужно сначала выполнить npm run build, а потом npm test. Да, это неудобно. В случае юнит-тестов этого делать не нужно, так как они запускаются под Node и не требуют трансляции и сборки.
Для полноты картины давайте взглянем на webpack.config.js, где описано, как Webpack должен делать сборку файлов:
module.exports = {
entry: './lib/app.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
...
}
Webpack будет читать наш app.js и собирать все необходимые файлы в bundle.js в папке dist.
Папка dist используется как в пользовательском окружении, так и в E2E-тестах. Это важно — запускать E2E-тесты нужно в средах, максимально похожих на «боевые».
Настройка браузера в Mocha
Наше приложение установлено на сервер — осталось лишь запустить для него браузер. Какую библиотеку мы будем использовать для автоматизации? Я обычно использую популярную selenium-webdriver.
Для начала давайте посмотрим, как мы используем её, прежде чем начнём разбираться с настройками:
const {prepareDriver, cleanupDriver} = require('../utils/browser-automation')
//...
describe('calculator app', function () {
let driver
...
before(async () => {
driver = await prepareDriver()
})
after(() => cleanupDriver(driver))
it('should work', async function () {
await driver.get('http://localhost:8080')
//...
})
})
В функции before мы готовим драйвер, а в after — очищаем его. Подготовка драйвера будет запускать браузер, а очистка — закрывать его. Заметим, что настройка драйвера происходит асинхронно и мы можем использовать async/await, чтобы сделать код красивее.
В тестовой функции мы открываем адрес http://localhost:8080, снова используя await, учитывая, что driver.get — асинхронная функция.
Так как же выглядят prepareDriver и cleanupDriver?
const webdriver = require('selenium-webdriver')
const chromeDriver = require('chromedriver')
const path = require('path')
const chromeDriverPathAddition = `:${path.dirname(chromeDriver.path)}`
exports.prepareDriver = async () => {
process.on('beforeExit', () => this.browser && this.browser.quit())
process.env.PATH += chromeDriverPathAddition
return await new webdriver.Builder()
.disableEnvironmentOverrides()
.forBrowser('chrome')
.setLoggingPrefs({browser: 'ALL', driver: 'ALL'})
.build()
}
exports.cleanupDriver = async (driver) => {
if (driver) {
driver.quit()
}
process.env.PATH = process.env.PATH.replace(chromeDriverPathAddition, '')
}
Это сложная штука. И я должен кое-что признать: этот код был написан кровью (о, и он работает только в Unix-системах). Он был написан при помощи Google, Stack Overflow и документации webdriver и сильно модифицирован методом научного тыка. Но он работает!
Теоретически вы можете просто скопипастить код в свои тесты, не разбираясь в нём, но давайте заглянем в него на секунду.
Первые две строки подключают webdriver — драйвер для браузера. Принцип работы Selenium Webdriver заключается в наличии API (в модуле selenium-webdriver, который мы импортируем в строке 1), который работает с любым браузером, и он полагается на драйверы браузера, чтобы… управлять различными браузерами. Драйвер, который я использовал, — chromedriver, импортированный в строке 2.
Драйвер Chrome не нуждается в браузере на машине: он фактически устанавливает свой собственный исполняемый файл Chrome, когда вы выполняете npm install. К сожалению, по некоторым причинам, которые я не могу понять, он не может найти его, и каталог chromedriver должен быть добавлен в PATH (это именно то, что не работает в Windows). Это мы делаем в строке 9. Мы также удаляем его из PATH на этапе очистки, в строке 22.
Итак, мы настроили драйвер браузера. Теперь пришло время настроить (и вернуть) веб-драйвер, что мы и делаем в строках 11–15. А поскольку функция build асинхронна и возвращает , мы ждём её при помощи await.
Почему мы делаем это в строках 11–15? Причины скрыты туманом опыта. Не стесняйтесь копипастить — никаких гарантий не прилагается, но я использовал этот код некоторое время, и проблем не возникало.
Приступим к тестам
Мы закончили настройку — пришло время взглянуть на код, который использует webdriver для управления браузером и тестирования нашего кода.
Разберём по частям:
// ...
const retry = require('promise-retry')
// ...
it('should work', async function () {
await driver.get('http://localhost:8080')
await retry(async () => {
const title = await driver.getTitle()
expect(title).to.equal('Calculator')
})
//...
Пропустим установку, которую мы видели раньше, и перейдем к самой тестовой функции.
Код переходит к приложению и проверяет, что его название — «Calculator». Первую строку мы уже видели — мы открываем наше приложение с помощью драйвера. И не забываем дождаться окончания процесса.
Перейдём к строке 9. Здесь мы просим браузер вернуть нам заголовок (используем await для ответа, потому что это асинхронно), а в строке 10 мы проверяем, что заголовок title имеет корректное значение.
Так почему мы повторяем это, используя модуль promise-retry? Причина очень важна, мы увидим, что и в остальной части теста браузер, когда мы попросим его что-то сделать, например, перейти по URL-адресу, сделает это, но асинхронно. Не позволяйте await одурачить вас! Мы ждём, пока браузер скажет: «OK, я сделал это», — а не конца операции.
Поиск элементов
Дальше, к следующей части теста!
const {By} = require('selenium-webdriver')
it('should work', async function () {
await driver.get('http://localhost:8080')
//...
await retry(async () => {
const displayElement = await driver.findElement(By.css('.display'))
const displayText = await displayElement.getText()
expect(displayText).to.equal('0')
})
//...
Теперь мы проверим, что первоначально display равен 0. Для этого найдем элемент, который содержит display — в нашем случае это класс display. Это мы делаем в строке номер 7 с помощью функции findelement объекта класса webdriver. Мы можем искать элементы с помощью методов By.id, By.css или . Я обычно использую By.css — он принимает селектор и очень гибок в использовании, хотя By.javascript, вероятно, самый гибкий из них.
Как вы могли заметить, By импортирован из selenium-webdriver.
В строке 10 с помощью метода getText() мы получаем содержимое элемента и проверяем его. Помните, что нужно дожидаться (await) выполнения всех методов!
Пользовательский интерфейс
Настало время тестировать наше приложение — нажимать на цифры и операторы и проверять результат операций:
const digit4Element = await driver.findElement(By.css('.digit-4'))
const digit2Element = await driver.findElement(By.css('.digit-2'))
const operatorMultiply = await driver.findElement(By.css('.operator-multiply'))
const operatorEquals = await driver.findElement(By.css('.operator-equals'))
await digit4Element.click()
await digit2Element.click()
await operatorMultiply.click()
await digit2Element.click()
await operatorEquals.click()
await retry(async () => {
const displayElement = await driver.findElement(By.css('.display'))
const displayText = await displayElement.getText()
expect(displayText).to.equal('84')
})
Сначала мы находим элементы, на которые хотим нажать, в строках 2–4. Затем нажимаем на них в строках 6–7. В нашем тесте получилось выражение "42*2=". Затем мы повторяем процесс, пока не получим правильный результат, "84".
Выполнение всех тестов
Итак, у нас есть E2E-тесты и юнит-тесты, запустим их с помощью npm test:
Всё отлично!
— .