Следующий пост адаптирован для Medium из моего личного блога.

Когда я начал переписывать процесс сборки своего веб-сайта, одной из моих целей было иметь полный набор интеграционных и браузерных тестов, то есть тестов, которые буквально запускались бы в реальном веб-браузере.

Как и в случае с тестированием в целом, теория управления браузером часто представляется чем-то загадочным или трудным для изучения. Но это намного проще, чем вы могли ожидать.

Сводка TLDR

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

  • запуск браузера (желательно безголового)
  • связь с этим браузером через Протокол веб-драйвера
  • переход в этом браузере к вашему собственному развертыванию

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

Что такое безголовый браузер и где его взять?

Большинство современных браузеров поставляются с клиентами или имеют параметры командной строки, позволяющие управлять собой через API. Нет ничего особенного в том, что происходит, когда среда тестирования запускает браузер. Они просто находят исполняемый файл на вашем компьютере, а затем запускают его с соответствующими аргументами командной строки.

При тестировании браузера браузер действительно выполняет большую часть тяжелой работы. Такие инструменты, как Selenium, не заходят и не редактируют память вашего компьютера — они просто интерфейсы между вами и браузером, который вы запускаете.

Безголовый браузер не нужен ни для одного кода, который я собираюсь вам показать. Вы можете подключить и управлять большинством браузеров: Chrome, Firefox, даже IE. Безголовый браузер — это браузер, который запускается без графического интерфейса. Безголовые браузеры часто используются в тестах браузеров, потому что они уменьшают количество зависимостей, которые должен запустить браузер, и потому что для их запуска часто требуется меньше системных ресурсов.

Кроме того, они просто чувствуют себя чище, понимаете? Они делают так, что у вас не появляется дополнительное окно на экране каждый раз, когда вы запускаете тест.

Популярность PhantomJS упала с тех пор, как был анонсирован Headless Chrome, но это все еще более легкий и простой браузер, который можно просто включить в проект Node, а затем запустить, особенно с учетом того, что команда Headless Chrome технически еще не избавилась от всех своих графических зависимостей. . Итак, мы будем использовать PhantomJS.

var phantomjs = require('phantomjs-prebuilt');
var browser = phantomjs.run('--webdriver=4444');

Приведенный выше код запускает браузер, к которому я могу подключиться через порт 4444. Подробнее об этом чуть позже.

Имейте в виду, что даже в приведенном выше примере все, что делает phantomjs-prebuilt, — это запускает процесс node и запускает исполняемый файл phantomjs с параметром командной строки. Вообще ничего волшебного.

Итак, теперь мне нужна библиотека для управления браузером?

И да и нет.

Выше я упоминал, что управлять им позволяет ваш браузер, а не Javascript. Когда мы будем управлять браузером, мы будем делать это с помощью Протокола веб-драйвера.

Webdriver не заботится о том, какой язык вы используете. Вы можете писать свои тесты на Python, Java, Lisp или даже Bash. Браузер просто прослушивает сетевые запросы и отвечает на них.

Неплохая идея узнать, как работает протокол веб-драйвера, и в будущем я могу сделать учебник более низкого уровня. Но для наших целей это перебор. При повседневном тестировании вы, вероятно, не заинтересованы в самостоятельной обработке сетевых запросов. Кроме того, помните, что мы говорим здесь о веб-браузерах, поэтому поддержка протокола веб-драйвера… не идеальна.

Мы будем полагаться на webdriverio, чтобы помочь нам отправлять эти сетевые запросы. Думайте об этом как о jQuery; это просто дополнительный уровень, который мы можем использовать, чтобы помочь стандартизировать поведение браузера, пока производители браузеров не разберутся со всем своим дерьмом.

Подождите, так это могут быть отдельные процессы?

Это хороший вопрос! Да, вы можете отправлять запросы веб-драйвера из совершенно отдельного процесса.

Помните, здесь мы говорим только о запросах на отдых. Вы никогда не будете напрямую взаимодействовать с браузером через Javascript. Чтобы понять это, инициализация для webdriverio запросит у нас только хост и порт.

var webdriverio = require('webdriverio');
var client = webdriverio.remote({
    desiredCapabilities : { browserName : 'phantomjs' },
    host : 'localhost',
    port : '4444'
});
//And now we can start sending browser commands.
var title = client.init().browse('https://google.com').title();
//Because we're sending requests over a network, our code becomes asynchronous.
title.then(function (title) {
    console.log(title);
});

Очевидный дополнительный вопрос: «Может ли мой браузер работать на отдельном компьютере?» Например, Safari на специальной виртуальной машине Macintosh. К сожалению, вы можете столкнуться с трудностями. Многие браузеры по умолчанию отключают удаленные подключения по протоколу webdriver. Иногда вы сможете переопределить это, но часто лучше просто настроить прокси-сервер на удаленном хосте и отправлять запросы через него, а не напрямую в браузер.

Это тема для другого учебника, а пока вам просто нужно поверить мне на слово, что проксирование запросов, вероятно, вполне в ваших силах.

Хорошо, я могу управлять браузером. Где мой сайт?

У вас есть браузер, и вы им управляете — это не значит, что ваш сайт будет развернут волшебным образом. Вам придется справиться с этим шагом самостоятельно.

Как и выше, это может происходить в отдельном процессе или на удаленном компьютере. Также возможно, что если вы делаете что-то вроде парсинга веб-страниц, у вас может даже не быть собственного сайта для развертывания, и в этом случае вы можете пропустить этот шаг.

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

Но если вы просто дурачитесь или не возражаете против дополнительного шага, не стесняйтесь просто открыть другое окно терминала и запустить Node http-server.

var http = require('http');
var Static = require('node-static');
var staticServer = new Static.Server('./public'), { cache : 0 });
var server = http.createServer(function (request, response) {
    staticServer.serve(request, response, function (err, result) {
        if (err) {
            response.writeHead(err.status, err.headers);
            response.end();
        }
    }).resume();
}).listen(8000);

Собираем все вместе

Хорошо, мы поговорили о теории. Срок реализации рабочий!

var phantomjs = require('phantomjs-prebuilt');
var webdriverio = require('webdriverio');
var http = require('http');
var Static = require('node-static');
/* Create our static server and tell it to listen (Step 3) */
var staticServer = new Static.Server('./public'), { cache : 0 });
var server = http.createServer(function (request, response) {
    staticServer.serve(request, response, function (err, result) {
        if (err) {
            response.writeHead(err.status, err.headers);
            response.end();
        }
    }).resume();
}).listen(8000, 'localhost', async function () {
    /* Start a browser process (Step 1) - remember, this could be any browser, headless or not. */
    var browser = await phantomjs.run('--webdriver=4444');
    /* (Step 2) Our client is a thin wrapper around the webdriver protocols. Think of it like jQuery for webdriver requests */
    var client = webdriverio.remote({
        desiredCapabilities : { browserName : 'phantomjs' },
        host : 'localhost',
        port : '4444'
    });
    client.init(); //library specific API, nothing special going on here.
    test(client).then(async function () {
        console.log('test passed');
    }, function (error) { /* test failed */
        console.log(error);
        process.exitCode = 1;
    }).then(async function () { /* Clean up, regardless of what happens (Step 4) */
        await client.end();
        await browser.kill();
        await server.close();
    });
});
async function test (client) {
    //Whatever testing library you're using.
    await client.url('http://localhost:8000');
    var title = await client.getTitle();
    if (title !== 'My Awesome Title') { throw new Error('ERROR!'); }

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

Если вы этого не сделаете, ваш браузер/сайт/клиент останется на связи после завершения тестов. Иногда вы можете действительно захотеть, чтобы это произошло, если вы выполняете отладку.

Идем дальше

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

  • Что произойдет, если я подключу несколько клиентов веб-драйвера одновременно?
  • С какой основной трудностью я столкнусь при параллельном выполнении тестов?
  • Что мне нужно сделать, если я хочу запустить несколько тестов на статическом сайте параллельно?
  • Что мне нужно сделать, если я хочу запустить несколько параллельных тестов на динамическом сайте (например, с учетными записями пользователей или постоянным состоянием)?
  • Что мне нужно сделать, если я хочу использовать одну команду для запуска тестов в нескольких браузерах?
  • Что, если бы эти браузеры находились на разных компьютерах (например, Mac и ПК)?

Если вы дочитали до этого места, вы, вероятно, хотя бы немного заинтересованы в низкоуровневом тестировании. Если да, то почему бы не заглянуть в Дистиллед? Это низкоуровневый фреймворк для тестирования, который отлично работает, если вы хотите создать свои собственные исполнители тестов, синтаксис или просто классные вещи в целом.