Студия разработки сайтов и приложений

Netspark.ru

CasperJS и Drupal

Сегодня мы немного поразбираемся в библиотеке CasperJS. Что такое CasperJS? Аннотация на сайте говорит нам, что это navigation scripting & testing utility, то есть инструмент для написания сценариев навигации и для тестирования. Работает CasperJS с помощью безбашенного headless-браузера PhantomJS (Webkit), либо с помощью почти-headless-браузера SlimerJS (Gecko).

Так что же с помощью CasperJS можно сделать? Например, можно написать вот такой скрипт:

casper.test.begin('Testing my website', function suite(test) {
  casper.start();
  casper.thenOpen('http://mytestsite.ru', function() {
    test.assertHttpStatus(200, 'Homepage was loaded successfully.');
    test.assertExists('div#logo', 'Header link to the homepage is present.');
    test.assertExists('form#user-login-form', 'Login form is present.');
  });
  casper.run(function() {
    test.done();
  });
});

Этот скрипт автоматически зайдет на главную страницу заданного сайта и проверит, что http-response равен 200, а затем проверит, что на странице присутствует div с логотипом и форма логина.

То есть, с помощью CasperJS мы можем описывать на обычном языке JavaScript сценарии последовательной навигации по сайту, с переходами по ссылкам, заполнением и отправкой форм, и конечно с тестовыми проверками (см. assert) на соответствие того, что мы видим, тем или иным условиям. Иными словами, CasperJS — это библиотека для front-end-тестирования.

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

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

В этом, на наш взгляд, и заключается основное назначение библиотеки:

  • в описании поведенческих паттернов, таких как «оформить заказ», «заполнить информацию в профиле», «создать материал»;
  • в автоматизированном запуске скриптов, исполняющих эти действия;
  • и в проверке результатов запуска на соответствие заданным условиям.

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

Кстати, CasperJS пригоден еще и для веб-скрейпинга, но это уже другая история.

На этом закончим затянувшееся вступление, и посмотрим на практике, как можно работать с библиотекой CasperJS в Друпале.

Установка CasperJS и PhantomJS

Установить CasperJS на сервер довольно просто:

  1. Нужно проверить наличие питона версии не менее 2.6 (и установить, если его нет):
python --version
  1. Затем установить CasperJS:
cd /usr/share
sudo git clone git://github.com/n1k0/casperjs.git
cd casperjs
git checkout 1.1-beta3
sudo ln -sf `pwd`/bin/casperjs /usr/local/bin/casperjs
  1. Затем нужно скачать последнюю версию PhantomJS с сайта, распаковать в /usr/share и выполнить:
cd /usr/share/phantomjs-1.9.8-linux-i686/bin
sudo ln -sf `pwd`/phantomjs /usr/local/bin/phantomjs
  1. В результате по запуску casperjs должна выводиться примерно такая надпись:

CasperJS version 1.1.0-beta3 at /usr/share/casperjs, using phantomjs version 1.9.8

Как выглядит скрипт CasperJS

Посмотрим на простенький скрипт с двумя тестами (что загрузилась главная страница и страница /user/login):

casper.test.begin('Testing my website', function suite(test) {
  casper.start();
  casper.thenOpen('http://mytestsite.ru', function() {
    test.assertHttpStatus(200, 'Homepage was loaded successfully.');
  });
  casper.thenOpen('http://mytestsite.ru/user/login', function() {
    test.assertHttpStatus(200, 'Login page was loaded successfully.');
  });
  casper.run(function() {
    test.done();
  });
});

Что мы видим?

casper.start() конфигурирует и запускает процесс, без этого нельзя.

casper.thenOpen() добавляет очередной шаг в цепочку навигационных действий (об этом ниже).

casper.run() запускает настроенную цепочку, а по окончании цепочки (в коллбэке, определенном в casper.run()) — происходит завершение тестирования test.done().

Такова структура одного теста на CasperJS: старт → определение цепочки навигационных действий → запуск этой цепочки → окончание теста (успешное или нет).

Теперь если мы запустим скрипт простой командой

casperjs test <путь и имя файла>

то увидим результат:

# Testing mysamplesite.ru
PASS Homepage was loaded successfully.
PASS Login page was loaded successfully.
PASS 2 tests executed in 1.423s, 2 passed, 0 failed, 0 dubious, 0 skipped.

Что важно знать о навигационных действиях

Здесь и далее, цепочкой навигационных действий мы будем называть последовательность действий в браузере, заданную методами casper.then*() и casper.waitFor*(). Этими действиями могут быть переходы по ссылкам, ожидание загрузки очередной страницы, или ресурса и т.п. Главное что каждый шаг в навигационной цепочке как правило сопряжен с событием загрузки чего-либо. И что именно в коллбэках, выполняемых по окончании загрузки этого чего-либо мы вызываем методы test.assert* для вычисления результатов теста.

Чтобы не сломать себе мозг в процессе отладки тестового скрипта на CasperJS, следует сразу понять важную вещь о навигационных действиях. А именно — то, что создание (инициализация) цепочки действий then-методами происходит до непосредственного запуска. Запуск заранее определенной цепочки действий происходит позже по вызову метода casper.run(). Очевидно, коллбэки, указанные для каждого шага цепочки, выполнятся асинхронно когда-нибудь потом, уже после инициализации.

То есть вот такой кусочек кода:

//...
var nextUrl;
function getMyUrl() {
  return 'http://google.com';
}
casper.thenOpen('http://yandex.ru', function () {
  nextUrl = getMyUrl();
});
casper.thenOpen(nextUrl, function () {
  casper.echo('Ну и ну!');
});
//...

не приведет к загрузке google.com, т.к. на момент инициализации навигационной цепочки переменная nextUrl еще не определена.

Чтобы можно было добавлять шаги в навигационную цепочку динамически — нужно вызывать casper.then*-методы прямо из коллбэков к другим casper.then*-методам.

Пара слов о evaluate()

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

Однако, зачастую нам нужно вычислить что-то непосредственно над DOM самой страницы, чтобы потом использовать в тестах. Для этого используется метод casper.evaluate(). По сути этот метод — мост между окружением CasperJS+PhantomJS и DOM открытой страницы. С помощью evaluate() мы можем выполнить какой-либо скрипт так, будто мы его выполняем из консоли браузера, а результат выполнения будет возвращен в окружение CasperJS для тестов. В скрипте checkout.js в конце заметки будет пример использования evaluate().

Для справки: если порыться в реализации assert*-методов типа assertExists(), нетрудно обнаружить, что их связь с DOM открытой страницы тоже реализована через evaluate().

То есть общее правило написания тестов на CasperJS: все манипуляции с DOM открытой страницы должны проходить через evaluate().

CasperJS для Drupal

Стараниями компании Lullabot создан фундамент для разработки тестов на CasperJS под Drupal. Фундамент существует в виде одноименного проекта и может быть установлен простой командой drush dl casperjs. Под фундаментом подразумевается не модуль, а команда casperjs для Drush, которую можно исполнять из любой директории сайта вот так:

drush casperjs

Помимо реализации команды для drush, в поставке фундамента есть несколько файлов с функциями-хелперами, а также пара стандартных тестов. Посмотрим на содержимое фундамента:

  • casperjs.drush.inc — реализация команды для drush;
  • includes/common.js — набор служебных функций, включая хелперы для сохранения скриншота, переключения вьюпортов и обертку над casper.run(); здесь же живет настройка таймаута на выход из зависания (равная по умолчанию двум минутам);
  • includes/session.js — функции, помогающие логиниться под тем или иным аккаунтом и обертка для casper.start(), позволяющая сразу начать тест с той или иной авторизации; здесь же можно в массиве casper.drupalUsers указать список пользователей и пароли для них, чтобы исполнять тесты с этих учетных записей;
  • includes/pre-test.js — скрипт, запускаемый перед всеми остальными тестами, в нем создаются сессии для всех пользователей из массива casper.drupalUsers;

    Важно: если вы хотите пользоваться командой drush casperjs и функциями-хелперами, нужно обязательно задать в массиве casper.drupalUsers файла includes/session.js хотя бы один актуальный логин-пароль, либо очистить массив. В скрипте pre-test.js проводится обязательная авторизация и она будет основательно подвисать, если что не так.

  • includes/post-test.js — скрипт, запускаемый после всех тестов, в нем принудительно закрываются все сессии для пользователей из массива casper.drupalUsers;
  • README.txt — в файле дана исчерпывающая и простая инструкция по установке CasperJS и PhantomJS на сервере, а также краткие сведения по функционалу и запуску тестов.

Еще в директории tests есть парочка базовых тестов — create_content.js и homepage.js. На них можно и нужно смотреть при написании своих скриптов. Однако запускать их особого смысла нет: они приведены для примера и без ошибок, зависаний и правок пройдут только на «ванильной» установке Drupal 7. Малейшее расхождение (настройка pathauto, тема, отличная от Бартика) — и тест не пройдет.

В целом заложенный луллаботами фундамент достаточно полезен. Разве что не очень нравится, что тесты надо держать в ~/.drush/casperjs/. Мало того что так тесты для разных проектов смешаются в кашу, так еще и запуск drush casperjs без аргументов будет норовить запустить для данного проекта всё что сможет найти в .drush/casperjs/tests/. Поэтому нам больше подошло выпилить из ~/.drush/casperjs/ все инклудники и подключать их отдельно в той структуре директорий с тестами, в которой удобно нам.

Тест оформления заказа checkout.js

Чтобы закрепить полученную информацию о CasperJS практикой — рассмотрим исполнение сценария оформления заказа в магазине на базе Drupal Commerce. В этом тесте мы возьмем случайный товар с главной страницы магазина, положим его в корзину, проверим, что он туда попал, а затем пройдем все стадии оформления заказа и найдем заказанный товар в профиле пользователя.

casper.test.begin('Проверяем оформление заказа', function suite(test) {

  var products = [];
  var cart = [];

  //Функция для получения массива объектов с информацией о товарах на заданной странице  
  function getProductsFromGrid() {
    var prods = [];
    jQuery('form.commerce-add-to-cart').each(function () {
      var product = {};
      var $productBlock = jQuery(this).parents('div.project-desc');
      product.title = $productBlock.find('h3 a').text();
      product.link = $productBlock.find('h3 a').attr('href');
      product.price = $productBlock.find('div.price div.field-content').text();
      product.formId = jQuery(this).attr('id');
      prods.push(product);
    });
    return prods;
  } //getProductsFromGrid

  //функция кладет заданный товар в корзину
  function putProductInCart(product) {
    jQuery('#' + product.formId).submit();
  }

  casper.drupalStartAs('test_user');

  casper.thenOpen('/', function() {
    //получаем товары на главной странице
    products = products.concat(this.evaluate(getProductsFromGrid));
    //проверяем, что товары найдены
    test.assertNotEquals(products.length, 0, 'Найдено ' + products.length + ' товаров на странице');
    //положим в корзину случайный товар
    var randomProduct = Math.floor(Math.random()*products.length + 0);
    cart.push(products[randomProduct]);
    //здесь и выше функции, использующие jQuery вызываются через casper.evaluate()
    //обратите внимание, как передаются аргументы
    this.evaluate(putProductInCart, cart[0]);
  });

  //проверим, что в корзине появился товар
  casper.thenOpen('/cart', function () {
    test.assertSelectorHasText('#dc-cart-ajax-form-wrapper table td.views-field-line-item-title div a', cart[0].title, 'Товар найден в корзине');
  });

  //проверим оформление заказа
  casper.thenClick('#edit-checkout');
  casper.waitForUrl(/checkout\/[0-9]+/, function () {
    test.assertExists('#commerce-checkout-form-shipping', 'Этап доставки на месте');
    //выберем "самовывоз" и отправим форму
    casper.fill('form#commerce-checkout-form-shipping',
                {'commerce_shipping[shipping_service]': 'takeaway'},
                true);
  });

  //проверим order review
  casper.waitForUrl(/checkout\/[0-9]+\/review/, function () {
    test.assertSelectorHasText('div.view-commerce-cart-summary table td.views-field-line-item-title', cart[0].title, 'В таблице проверки заказа найден выбранный ранее товар');
    casper.fill('form#commerce-checkout-form-review', {}, true);
  });
  //проверим форму заполнения информации о пользователе
  casper.waitForUrl(/checkout\/[0-9]+\/checkout/, function () {
    test.assertExists('#commerce-checkout-form-checkout', 'Форма оформления на месте');
    casper.fill('form#commerce-checkout-form-checkout',
                {
                  'customer_profile_billing[field_first_name][und][0][value]': 'Test',
                  'customer_profile_billing[field_last_name][und][0][value]': 'User',
                  'customer_profile_billing[field_phone][und][0][value]': '+7 (903) 123-45-67',
                  'customer_profile_billing[field_call_time][und][0][value]': '9 - 18',
                  'customer_profile_billing[field_postcode][und][0][value]': '124482',
                  'customer_profile_billing[field_area][und][0][value]': 'Москва',
                  'customer_profile_billing[field_city][und][0][value]': 'Зеленоград',
                  'customer_profile_billing[field_city_address][und][0][value]': 'Савелкинский проезд д. 4',
                  'customer_profile_billing[field_more_info][und][0][value]': 'Тестовый заказ: не звонить, не писать, проверять ошибки!'
                },
                true);
  });
  //проверим форму оплаты
  casper.waitForUrl(/checkout\/[0-9]+\/online_payment/, function () {
    test.assertExists('form#commerce-checkout-form-online-payment', 'Форма оплаты на месте');
    casper.fill('form#commerce-checkout-form-online-payment',
                {
                  'commerce_payment[payment_method]': 'cash|commerce_payment_cash'
                },
                true);
  });

  //проверим завершение заказа
  casper.waitForUrl(/checkout\/[0-9]+\/complete/, function () {
    test.assertExists('form#commerce-checkout-form-complete', 'Заказ оформлен');
    //ссылка на оформленный заказ
    var orderLink = this.evaluate(function () {
      return jQuery('div.checkout-completion-message a').attr('href');
    });
    casper.echo(orderLink);
    //зайдем на страницу заказа в профиле пользователя
    casper.thenOpen(orderLink, function () {
      test.assertSelectorHasText('div.entity-commerce-order table td.views-field-line-item-title', cart[0].title, 'Выбранный товар найден в оформленном заказе');
    });
  });

  casper.drupalRun();
});

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

Недостатки

Не то чтобы были налицо какие-то страшные изъяны, но пару особенностей стоит отметить.

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

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

Заключение

Что дальше можно делать с тестами на CasperJS? Можно подробнее описать несколько часто возникающих шаблонов действий (создание материалов, регистрация, поиск товара, вычисление скидки и т.п.), увеличить, так сказать, покрытие. Можно добавить небольшое API для извлечения данных из бэкэнда в целях тестирования. Можно написать тест, выполняющий рандомные действия, или последовательно обходящий url-ы из карты сайта в поиске ошибок и warning-ов — благо их легко поймать обычным селектором.

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

Комментарии