Статьи

Параллельное тестирование для PHPUnit с ParaTest

PHPUnit намекает на параллелизм с 2007 года, но, тем временем, наши тесты продолжают работать медленно. Время это деньги, верно? ParaTest — это инструмент, который находится поверх PHPUnit и позволяет вам запускать тесты параллельно без использования расширений. Это идеальный кандидат для функциональных (например, Selenium) тестов и других длительных процессов.


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

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

ParaTest также имеет честь быть в комплекте с тестовой средой Sauce Labs Sausage , и на момент написания этой статьи использовался почти в 7000 проектах.

В настоящее время единственный официальный способ установки ParaTest — через Composer . Для тех из вас, кто плохо знаком с Composer, у нас есть отличная статья на эту тему. Чтобы получить последнюю версию разработки, включите в файл composer.json :

Кроме того, для последней стабильной версии:

Затем запустите composer install из командной строки. Двоичный файл ParaTest будет создан в каталоге vendor/bin .

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

ParaTest CLI

Использовать ParaTest так же просто, как и PHPUnit. Чтобы быстро продемонстрировать это в действии, создайте каталог paratest-sample со следующей структурой:

пример структуры каталогов

Давайте установим ParaTest, как указано выше. Предполагая, что у вас есть оболочка Bash и глобально установленный двоичный файл Composer, вы можете выполнить это в одной строке из paratest-sample :

1
echo ‘{«require»: { «brianium/paratest»: «0.4.4» }}’ > composer.json && composer install

Для каждого из файлов в каталоге создайте класс тестового примера с таким же именем, например:

1
2
3
4
5
6
7
8
class SlowOneTest extends PHPUnit_Framework_TestCase
{
    public function test_long_running_condition()
    {
        sleep(5);
        $this->assertTrue(true);
    }
}

Обратите внимание на использование sleep(5) для имитации теста, выполнение которого займет пять секунд. Таким образом, у нас должно быть пять тестов, каждый из которых занимает пять секунд. Используя vanilla PHPUnit, эти тесты будут выполняться последовательно и займут всего двадцать пять секунд. ParaTest будет запускать эти тесты одновременно в пяти отдельных процессах и займет всего пять секунд, а не двадцать пять!

ParaTest vs Vanilla PHPUnit

Теперь, когда у нас есть понимание того, что такое ParaTest, давайте немного углубимся в проблемы, связанные с параллельным выполнением тестов PHPUnit.


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

Общий метод для достижения этой цели не является новой идеей: запускать разные тестовые группы в нескольких процессах PHPUnit. Это легко сделать с помощью встроенной функции PHP proc_open . Следующее будет примером этого в действии:

01
02
03
04
05
06
07
08
09
10
/**
 * $runningTests — currently open processes
 * $loadedTests — an array of test paths
 * $maxProcs — the total number of processes we want running
 */
while(sizeof($runningTests) || sizeof($loadedTests)) {
    while(sizeof($loadedTests) && sizeof($runningTests) < $maxProcs)
        $runningTests[] = proc_open(«phpunit » . array_shift($loadedTests), $descriptorspec, $pipes);
    //log results and remove any processes that have finished ….
}

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

  • Как мы загружаем тесты?
  • Как мы собираем и публикуем результаты различных процессов PHPUnit?
  • Как мы можем обеспечить согласованность с оригинальным инструментом (например, PHPUnit)?

Давайте рассмотрим несколько методов, которые использовались в прошлом, а затем рассмотрим ParaTest и то, как он отличается от остальной части толпы.


Как отмечалось ранее, идея запуска PHPUnit в нескольких процессах не нова. Типичная процедура заключается в следующем:

  • Grep для тестовых методов или загрузите каталог файлов, содержащих тестовые наборы.
  • Откройте процесс для каждого метода тестирования или набора.
  • Разобрать вывод из трубы STDOUT.

Давайте посмотрим на инструмент, который использует этот метод.

Paraunit был оригинальным параллельным раннером в комплекте с колбасным инструментом Sauce Labs, и он служил отправной точкой для ParaTest. Давайте посмотрим, как она решает три основные проблемы, упомянутые выше.

Paraunit был разработан для облегчения функционального тестирования. Он выполняет каждый метод тестирования, а не весь набор тестов в собственном процессе PHPUnit. Учитывая путь к набору тестов, Paraunit ищет отдельные методы тестирования, сопоставляя шаблоны с содержимым файла.

1
preg_match_all(«/function (test[^\(]+)\(/», $fileContents, $matches);

Загруженные методы тестирования могут быть запущены следующим образом:

1
proc_open(«phpunit —filter=$testName $testFile», $descriptorspec, $pipes);

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

Хотя методы, начинающиеся со слова « тест », являются строгим соглашением среди пользователей PHPUnit, аннотации являются еще одним вариантом. Метод загрузки, используемый Paraunit, пропустит этот совершенно правильный тест:

1
2
3
4
5
6
7
8
9
/**
 * @test
 */
public function twoTodosCheckedShowsCorrectClearButtonText()
{
    $this->todos->addTodos(array(‘one’, ‘two’));
    $this->todos->getToggleAll()->click();
    $this->assertEquals(‘Clear 2 completed items’, $this->todos->getClearButton()->text());
}

Помимо отсутствия поддержки тестовых аннотаций, наследование также ограничено. Мы могли бы поспорить о достоинствах сделать что-то подобное, но давайте рассмотрим следующую настройку:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
abstract class TodoTest extends PHPUnit_Extensions_Selenium2TestCase
{
    protected $browser = null;
 
    public function setUp()
    {
       //configure browser
    }
 
    public function testTypingIntoFieldAndHittingEnterAddsTodo()
    {
        //selenium magic
    }
}
 
/**
 * ChromeTodoTest.php
 * No test methods to read!
 */
class ChromeTodoTest extends TodoTest
{
    protected $browser = ‘chrome’;
}
 
/**
 * FirefoxTodoTest.php
 * No test methods to read!
 */
class FirefoxTodoTest extends TodoTest
{
    protected $browser = ‘firefox’;
}

Унаследованные методы отсутствуют в файле, поэтому они никогда не будут загружены.

Paraunit объединяет результаты каждого процесса, анализируя выходные данные, генерируемые каждым процессом. Этот метод позволяет Paraunit захватить весь спектр коротких кодов и отзывов, представленных PHPUnit.

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

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

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


Целью ParaTest является поддержка параллельного тестирования для различных сценариев. Первоначально созданный для устранения пробелов в Paraunit, он стал надежным инструментом командной строки для параллельной работы как наборов тестов, так и методов тестирования. Это делает ParaTest идеальным кандидатом для длительных испытаний различных форм и размеров.

ParaTest отклоняется от установленной нормы для поддержки большего количества PHPUnit и действует как действительно жизнеспособный кандидат для параллельного тестирования.

ParaTest загружает тесты аналогично PHPUnit. Он загружает все тесты в указанном каталоге, которые заканчиваются суффиксом *Test.php , или загружает тесты на основе стандартного XML-файла конфигурации PHPUnit. Загрузка осуществляется посредством отражения, поэтому легко поддерживать методы @test , наследование, наборы тестов и отдельные методы тестирования. Отражение делает добавление поддержки других аннотаций проще простого.

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

ParaTest накладывает некоторые ограничения, но обоснованные в сообществе PHP. Тесты должны соответствовать стандарту PSR-0 , а суффикс файла по умолчанию *Test.php не настраивается, как в PHPUnit. В настоящее время выполняется ветка для поддержки той же конфигурации суффикса, которая разрешена в PHPUnit.

ParaTest также отклоняется от пути анализа STDOUT-труб. Вместо анализа выходных потоков ParaTest регистрирует результаты каждого процесса PHPUnit в формате JUnit и объединяет результаты из этих журналов. Гораздо проще прочитать результаты теста из установленного формата, чем выходной поток.

1
2
3
4
5
6
7
8
<?xml version=»1.0″ encoding=»UTF-8″?>
<testsuites>
  <testsuite name=»AnotherUnitTestInSubLevelTest» file=»/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php» tests=»3″ assertions=»3″ failures=»0″ errors=»0″ time=»0.005295″>
    <testcase name=»testTruth» class=»AnotherUnitTestInSubLevelTest» file=»/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php» line=»7″ assertions=»1″ time=»0.001739″/>
    <testcase name=»testFalsehood» class=»AnotherUnitTestInSubLevelTest» file=»/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php» line=»15″ assertions=»1″ time=»0.000477″/>
    <testcase name=»testArrayLength» class=»AnotherUnitTestInSubLevelTest» file=»/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php» line=»23″ assertions=»1″ time=»0.003079″/>
  </testsuite>
</testsuites>

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

Отражение позволяет ParaTest поддерживать больше соглашений PHPUnit. Консоль ParaTest поддерживает больше функций PHPUnit из коробки, чем любой другой аналогичный инструмент, такой как возможность запуска групп, предоставления файлов конфигурации и начальной загрузки и регистрации результатов в формате JUnit.


ParaTest можно использовать для увеличения скорости в нескольких сценариях тестирования.

ParaTest выделяется при функциональном тестировании. Он поддерживает ключ -f в своей консоли, чтобы включить функциональный режим. Функциональный режим предписывает ParaTest запускать каждый метод тестирования в отдельном процессе, а не по умолчанию, то есть запускать каждый набор тестов в отдельном процессе.

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

Пример проекта, paratest-selenium , демонстрирует тестирование приложения todo Backbone.js с Selenium и ParaTest. Каждый метод тестирования открывает браузер и тестирует определенную функцию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public function setUp()
{
    $this->setBrowserUrl(‘http://backbonejs.org/examples/todos/’);
    $this->todos = new Todos($this->prepareSession());
}
 
public function testTypingIntoFieldAndHittingEnterAddsTodo()
{
    $this->todos->addTodo(«parallelize phpunit tests\n»);
    $this->assertEquals(1, sizeof($this->todos->getItems()));
}
 
public function testClickingTodoCheckboxMarksTodoDone()
{
    $this->todos->addTodo(«make sure you can complete todos»);
    $items = $this->todos->getItems();
    $item = array_shift($items);
    $this->todos->getItemCheckbox($item)->click();
    $this->assertEquals(‘done’, $item->attribute(‘class’));
}
 
//….more tests

Этот тестовый случай может занять много секунд, если он будет работать последовательно, через vanilla PHPUnit. Почему бы не запустить несколько методов одновременно?

запуск тестов селена с ParaTest
Многие экземпляры Chrome выполняют функциональные тесты

Как и при любом параллельном тестировании, мы должны помнить о сценариях, которые будут представлять условия гонки, такие как несколько процессов, пытающихся получить доступ к базе данных. В ветке dev-master ParaTest реализована действительно удобная функция тестовых токенов, написанная сотрудником Dimitris Baltas (dbaltas на Github), которая значительно упрощает интеграционное тестирование баз данных.

Димитрис включил полезный пример, демонстрирующий эту функцию на Github . По словам самого Димитриса:

TEST_TOKEN пытается решить проблему с общими ресурсами очень простым способом: клонировать ресурсы, чтобы гарантировать, что никакие параллельные процессы не получат доступ к одному и тому же ресурсу.

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

1
2
3
4
5
public function setUp()
{
    parent::setUp();
    $this->_filename = sprintf(‘out%s.txt’, getenv(‘TEST_TOKEN’));
}

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

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


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

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

ParaTest имеет много хороших вещей в работе, чтобы повысить прозрачность между PHPUnit и самим собой, прежде всего в том, какие параметры конфигурации поддерживаются.

Последняя стабильная версия ParaTest (v0.4.4) удобно поддерживает Mac, Linux и Windows, но в dev-master есть несколько полезных запросов и функций, которые определенно обслуживают толпы Mac и Linux. Так что это будет интересный разговор в будущем.

В Интернете есть несколько статей и ресурсов, посвященных ParaTest. Дайте им прочитать, если вам интересно: