Статьи

Параллельный PHPUnit

PHPUnit — это стандартная среда тестирования для кода PHP: всегда доступна через Pear или Composer, следуя соглашениям xUnit по тестам и предоставляя множество функций от группировки до покрытия кода и регистрации результатов. Есть даже расширение для запуска тестов Selenium (которое я поддерживаю), которое позволяет вам запускать тесты на основе браузера.

параллелизм

В PHPUnit отсутствует параллелизм: тесты запускаются один за другим, как правило, в одном и том же процессе. Это означает, что когда у вас есть больше доступных ресурсов, таких как многоядерный процессор, некоторая вычислительная мощность не используется, поскольку процесс PHPUnit может достигать 100% использования, в то время как другие ядра вообще не работают. Это не удивительно.

PHP не имеет многопоточности, но может запускать новые процессы на уровне ОС. Многие разработчики выдвинули одну и ту же идею: запускать несколько процессов PHPUnit, каждый из которых работает над своим подмножеством тестов, и объединять результаты. Это теоретически может дать вам N-кратное ускорение при работе с N различными ядрами, например, переход от 10 минут на одном ядре к 2’30 » на четырехъядерном процессоре.

Предостережения

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

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

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

public function setUp()
{
  $this->pdo = new PDO(...);
  $this->pdo->query('DELETE FROM users');
}

public function testUsersCanBeAddedWithAllDetails()
{
  $this->request->post('/users', ...);
  $this->assertEquals(1, $this->request->get('/users'));
}

public function testUsersCanBeDeletedByAnAdmin()
{
  $this->insertAnUser();
  $this->assertEquals(1, $this->request->get('/users'));
  $this->request->delete('/users', ...);
  $this->assertEquals(0, $this->request->get('/users'));
}

Эти тесты на основе API никогда не будут выполняться параллельно (на одной и той же машине) при такой записи из-за состояния гонки в таблице пользователей . Если у вас есть медленный набор, который вы хотите ускорить, есть вероятность, что он содержит множество сквозных тестов, подобных этим. Некоторые из этих тестов могут быть изолированы с помощью транзакций СУБД, но тестам черного ящика сложно вмешиваться в изоляцию транзакций внутри приложения.

Инструменты

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

  • Он использует рефлексию для составления списка всех ваших тестов вместо grepping * Test.php файлов.
  • Он считывает журналы PHPUnit в формате JUnit для агрегирования результатов различных тестов, что затрудняет его разбивание по сравнению с инструментами, которые анализируют выходные данные самой команды.

Единственным ограничением этого является то, что оно накладывает более жесткие ограничения на ваши тесты, например, они должны следовать соглашению PSR-0. Однако он много делегирует PHPUnit и позволяет вам использовать многие из тех же ключей командной строки, как —configuration и —bootstrap.

Эксперименты

Чтобы поэкспериментировать с Paratest, я создал набор имитированных модульных тестов, который работает только с процессором. У меня есть 10 тестов формы :

public function testExample()
{
    for ($i = 0; $i < 1024*1024; $i++) { $this->assertTrue(true); }
}

Затем я попытался запустить этот пакет на двухъядерном процессоре, на физической (не виртуальной) домашней машине. Я тоже пробовал разные варианты:

  1. Ванильный PHPUnit, серийное исполнение
  2. Paratest, одиночное выполнение процесса (чтобы узнать, имеет ли он высокие издержки).
  3. Паратест с 2 параллельными процессами.

Вот результаты:

[21:13:25][giorgio@Desmond:~/paratestexample]$ ./compare.sh
PHPUnit 3.7.13-5-g6937c46 by Sebastian Bergmann.

..........

Time: 03:04, Memory: 3.25Mb

OK (10 tests, 20971520 assertions)

Running phpunit in 1 process with /home/giorgio/paratestexample/vendor/bin/phpunit

..........

Time: 03:01, Memory: 3.75Mb

OK (10 tests, 20971520 assertions)

Running phpunit in 2 processes with /home/giorgio/paratestexample/vendor/bin/phpunit

..........

Time: 02:15, Memory: 3.75Mb

OK (10 tests, 20971520 assertions)

Разница в уменьшении общего времени на 25%, что действительно заслуживает дальнейшего изучения.

Выводы

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