Запускать тесты PHPUnit в определенном порядке

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


person dragonmantank    schedule 13.08.2008    source источник
comment
Вы можете добавить @depends, как описано в ответе ниже, и использование setup() и teardown() также является хорошей идеей, но тесты просто выполняются сверху вниз...   -  person Andrew    schedule 10.06.2015
comment
Еще один вариант использования, который, кажется, не был рассмотрен: возможно, все тесты атомарны, но некоторые тесты МЕДЛЕННЫ. Я хочу, чтобы быстрые тесты запускались как можно скорее, чтобы они могли быстро дать сбой, а любые медленные тесты запускались в последнюю очередь, после того как я уже увижу другие проблемы и смогу немедленно их решить.   -  person Kzqai    schedule 20.03.2016


Ответы (8)


Возможно, в ваших тестах есть проблема с дизайном.

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

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

Можете ли вы уточнить, почему вам нужен один и тот же объект для N тестов?

person Fabio Gomes    schedule 13.08.2008
comment
Это не кажется мне правильным. Суть юнит-теста в том, чтобы протестировать весь юнит. Смысл наличия единицы состоит в том, чтобы сгруппировать вместе вещи, которые должны зависеть друг от друга. Написание тестов, которые проверяют отдельные методы без контекста для класса, сродни защите процедурного программирования по сравнению с oo, потому что вы выступаете за то, чтобы отдельные функции не зависели от одних и тех же данных. - person doliver; 05.05.2013
comment
Я не согласен с вашей точкой зрения. Результатом проверки экземпляра является допустимый объект, который может использоваться другими тестами в вашем наборе тестов. Нет необходимости создавать экземпляр нового объекта для каждого теста, особенно если конструктор сложный. - person pedromanoel; 12.07.2013
comment
Если конструктор сложен, вы делаете что-то не так, возможно, ваш класс делает слишком много. Пожалуйста, прочитайте о SOLID, более подробно о шаблоне единой ответственности (SRP), также вы должны подделывать зависимости в своих тестах, используя моки, также читайте о моках, подделках и заглушках. - person Fabio Gomes; 14.07.2013
comment
Может быть и практическая причина. Например, если необходимая вам очистка занимает особенно много времени, вы можете использовать функцию tearDownAfterClass, чтобы запустить ее только один раз. Если какой-то конкретный тест требует «чистого листа», вам нужно либо убедиться, что этот тест запускается первым, либо вручную вызвать функцию tearDownAfterClass при его запуске, что приведет к его двойному запуску. Да, это, вероятно, признак того, что что-то не так с дизайном тестового класса, но есть законные случаи, когда упорядочивание тестов полезно. - person Benubird; 23.07.2014
comment
По крайней мере, для тестирования базы данных часто необходимо повторное использование объектов (по крайней мере, соединения). PHPUnit также ссылается на это: phpunit.de/manual/current/en/database.html (см. Совет: используйте собственный тестовый пример абстрактной базы данных) - person emfi; 28.07.2017
comment
@emfi, если вы снова тестируете реальную базу данных, вы не выполняете модульные тесты. Вы выполняете функциональные тесты. Чтобы выполнить модульные тесты, вы должны имитировать адаптер БД и, как говорит Фабио, вам нужно создавать экземпляр вашей SUT (тестируемой системы) при каждом тестовом прогоне. Вы можете использовать защищенный метод setUp() для подготовки макетов, если есть что-то, что вы собираетесь повторять для каждого теста. - person Xavi Montero; 31.03.2018
comment
Я не согласен - я тестирую процесс, в котором виджет должен быть отправлен одним человеком и одобрен другим, поэтому они должны быть в порядке. - person NULL pointer; 21.08.2018
comment
@Xavi Montero: при фактической разработке построителя запросов и / или ORM вам необходимо выполнить запросы в соответствующей СУБД для полного теста. в данном конкретном случае я думаю, что это считается модульным тестированием. - person emfi; 15.02.2019
comment
@emfi, если код, который вы тестируете, является на самом деле мостом к БД (например, ORM, ODM и т. д.) (например, вы являетесь разработчиком в проекте Doctrine или ваша компания разрабатывает Заменитель Doctrine) вы, вероятно, будете издеваться над объектом соединения и тестировать, как используется соединение, чтобы увидеть, какие запросы создаются и т. д. Единственным тестом, который может иметь запись в реальной БД, может быть тест для самого объекта соединения. , и он получит доступ к очень контролируемому реальному соединению с ограниченными тестами. - person Xavi Montero; 16.02.2019
comment
@emfi, но если вы ссылаетесь на классы тестирования, которые обычно обращаются к ORM, вы должны издеваться над самой ORM. Вместо этого часто сложно имитировать ORM, и для простоты мы хотим, чтобы ORM был реальным (а не имитированным) и переходил к реальным INSERT или SELECT. [продолжение в следующем комментарии] - person Xavi Montero; 16.02.2019
comment
Хорошо, я согласен с вами: в некоторых конкретных случаях выполнение реального доступа к БД может считаться модульным тестом, а не функциональным тестом со следующими соображениями: [продолжение в следующем комментарии] - person Xavi Montero; 16.02.2019
comment
а) Мы знаем, что нарушаем модульный тест, так как он проверяет потребляющий_класс+ORM, а не только потребляющий_класс. Мы допустим это тогда и только тогда, когда ORM будет полностью протестирован. Тестирование одного-единственного класса ближе к модульному тестированию, а тестирование двух или более классов ближе к интеграционному тестированию. - person Xavi Montero; 16.02.2019
comment
б) Если мы расширим DBUnit и используем, например, утилиты наборов данных для создания наборов данных и предпочтительно используем sqlite в памяти, это будет ближе к модульному тестированию, но если мы подключимся к тестовой базе данных mysql с копией производственного данные, это ближе к функциональному тестированию - person Xavi Montero; 16.02.2019
comment
c) Но для меня, наконец, содержание теста — это то, что определяет, являемся ли мы единицами или функциональными. Если мы протестируем одну операцию; например, адаптер электронной почты регистрирует в базе данных событие, в котором говорится, что электронное письмо отправлено сразу после возврата из SMTP-вызова; и скажем, что мы издеваемся над SMTP, но не издеваемся над БД для простоты; это может быть модульный тест, потому что мы тестируем один единственный метод в одном классе EmailAdapter->sendEmail() и проверяем, как это изменило состояние. [продолжает] - person Xavi Montero; 16.02.2019
comment
Вместо этого, если мы проверим, например, что после сохранения объекта мы можем запросить его, это НЕ модуль, а функциональность. Мы тестируем что-то вроде $adapter->save( $object );, а затем $actualObject = $adapter->loadById( $id ); — это проверка комбинации двух методов, и это функциональное тестирование, а не модульное тестирование. Если мы хотим конкретно придерживаться модуля, его следует разделить на 2 теста: по одному для каждого модуля: тест 1) с настройкой, очищающей БД, сохраняя и подтверждая, что БД содержит данные, тест 2) установка устанавливает фикстуру, загрузку и подтвердите, что загруженный объект соответствует прибору. - person Xavi Montero; 16.02.2019

PHPUnit поддерживает тестовые зависимости через @depends аннотация.

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

class StackTest extends PHPUnit_Framework_TestCase
{
    public function testEmpty()
    {
        $stack = array();
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}

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

person mjs    schedule 16.12.2009
comment
Для PHPUnit это означает, что тестовая функция будет пропущена, если предыдущий тест не был выполнен. Это не создает тестовый заказ. - person Dereckson; 09.02.2014
comment
Просто чтобы расширить @Dereckson, аннотация @depends приведет к тому, что тест будет пропущен, если тест, который зависит от либо, еще не был запущен или не прошел, когда он выполнялся. - person km6zla; 21.08.2014
comment
@ km6zla означает ли это, что если мы поместим или напишем метод testPop() перед testPush() в файле, тогда testPop() никогда не будет выполняться и всегда будет пропущен? - person Top-Master; 28.03.2019

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

phpunit.xml:

<phpunit
        colors="true"
        bootstrap="./tests/bootstrap.php"
        convertErrorsToExceptions="true"
        convertNoticesToExceptions="true"
        convertWarningsToExceptions="true"
        strict="true"
        stopOnError="false"
        stopOnFailure="false"
        stopOnIncomplete="false"
        stopOnSkipped="false"
        stopOnRisky="false"
>
    <testsuites>
        <testsuite name="Your tests">
            <file>file1</file> //this will be run before file2
            <file>file2</file> //this depends on file1
        </testsuite>
    </testsuites>
</phpunit>
person Gino Pane    schedule 26.07.2017
comment
я думаю, что это единственное надежное решение - person emfi; 28.07.2017
comment
Идеально! Не каждый тест является модульным тестом; например, при написании тестов HTTP-запросов или функций может потребоваться сохранение изменений состояния в классах тестов, и в таких случаях это самый надежный подход к запуску тестов в осмысленной последовательности. - person Ben Johnson; 13.08.2018
comment
Кто-нибудь еще тестировал, верно ли это для параллельного выполнения тестов PHPUnit? - person Smamatti; 24.11.2019
comment
Означает ли этот ответ, что каждый отдельный тестовый файл должен быть явно указан, даже если существуют сотни тестовых файлов? Это не выглядит хорошим решением. - person Attila Szeremi; 24.03.2021
comment
@AttilaSzeremi, к сожалению, да. С тех пор я изучал эту проблему, так что, возможно, сейчас есть лучшая. Я считаю, что лучше иметь работающее (хотя и не идеальное) решение, чем вообще никакого решения :) - person Gino Pane; 24.03.2021

Если вы хотите, чтобы ваши тесты совместно использовали различные вспомогательные объекты и настройки, вы можете использовать setUp(), tearDown() для добавления к свойству sharedFixture.

person Gary Richardson    schedule 23.08.2008
comment
Можно еще assertEquals() и т. д. в setUp()? Это плохая практика? - person jchook; 18.12.2016

PHPUnit позволяет использовать аннотацию @depends, которая определяет зависимые тестовые случаи и позволяет передавать аргументы между зависимыми тестовыми примерами.

person saleem badreddine    schedule 29.12.2009

Альтернативное решение: используйте статические (!) функции в своих тестах для создания повторно используемых элементов. Например (я использую selenium IDE для записи тестов и phpunit-selenium (github) для запуска тестов внутри браузера)

class LoginTest extends SeleniumClearTestCase
{
    public function testAdminLogin()
    {
        self::adminLogin($this);
    }

    public function testLogout()
    {
        self::adminLogin($this);
        self::logout($this);
    }

    public static function adminLogin($t)
    {
        self::login($t, '[email protected]', 'pAs$w0rd');
        $t->assertEquals('John Smith', $t->getText('css=span.hidden-xs'));
    }

    // @source LoginTest.se
    public static function login($t, $login, $pass)
    {
        $t->open('/');
        $t->click("xpath=(//a[contains(text(),'Log In')])[2]");
        $t->waitForPageToLoad('30000');
        $t->type('name=email', $login);
        $t->type('name=password', $pass);
        $t->click("//button[@type='submit']");
        $t->waitForPageToLoad('30000');
    }

    // @source LogoutTest.se
    public static function logout($t)
    {
        $t->click('css=span.hidden-xs');
        $t->click('link=Logout');
        $t->waitForPageToLoad('30000');
        $t->assertEquals('PANEL', $t->getText("xpath=(//a[contains(text(),'Panel')])[2]"));
    }
}

Хорошо, и теперь я могу использовать эти многократно используемые элементы в другом тесте :) Например:

class ChangeBlogTitleTest extends SeleniumClearTestCase
{
    public function testAddBlogTitle()
    {
      self::addBlogTitle($this,'I like my boobies');
      self::cleanAddBlogTitle();
    }

    public static function addBlogTitle($t,$title) {
      LoginTest::adminLogin($t);

      $t->click('link=ChangeTitle');
      ...
      $t->type('name=blog-title', $title);
      LoginTest::logout($t);
      LoginTest::login($t, '[email protected]','hilton');
      $t->screenshot(); // take some photos :)
      $t->assertEquals($title, $t->getText('...'));
    }

    public static function cleanAddBlogTitle() {
        $lastTitle = BlogTitlesHistory::orderBy('id')->first();
        $lastTitle->delete();
    }
  • Таким образом, вы можете построить иерархию ваших тестов.
  • Вы можете сохранить свойство, что каждый тестовый пример полностью отделен от другого (если вы очищаете БД после каждого теста).
  • И самое главное, если, например, способ входа в систему изменится в будущем, вы только модифицируете класс LoginTest, и вам не нужна правильная часть входа в другие тесты (они должны работать после обновления LoginTest) :)

Когда я запускаю тест, мой сценарий очищает базу данных в начале. Выше я использую свой класс SeleniumClearTestCase (я делаю скриншот() и другие приятные функции там), это расширение MigrationToSelenium2 (из github для переноса записанных тестов в firefox с использованием плагина seleniumIDE + ff "Selenium IDE: PHP Formatters"), который является расширением мой класс LaravelTestCase (это копия Illuminate\Foundation\Testing\TestCase, но не расширяет PHPUnit_Framework_TestCase), который настраивает laravel для доступа к красноречивому, когда мы хотим очистить БД в конце теста), который является расширением PHPUnit_Extensions_Selenium2TestCase. Чтобы настроить laravel eloquent, у меня также есть в SeleniumClearTestCase функция createApplication (которая вызывается в setUp, и я беру эту функцию из laral test/TestCase)

person Kamil Kiełczewski    schedule 17.06.2016
comment
Вот более подробная информация о запуске теста, записанного в Selenium IDE на Laravel 5.2 и phpUnit: stackoverflow.com/questions/33845828/ - person Kamil Kiełczewski; 19.06.2016

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

Изначально у меня было два метода: а. testCreateResource и б. testDestroyResource

а. testCreateResource

<?php
$app->createResource('resource');
$this->assertTrue($app->hasResource('resource'));
?>

б. testDestroyResource

<?php
$app->destroyResource('resource');
$this->assertFalse($app->hasResource('resource'));
?>

Я думаю, что это плохая идея, так как testDestroyResource зависит от testCreateResource. И лучшей практикой было бы сделать

а. testCreateResource

<?php
$app->createResource('resource');
$this->assertTrue($app->hasResource('resource'));
$app->deleteResource('resource');
?>

б. testDestroyResource

<?php
$app->createResource('resource');
$app->destroyResource('resource');
$this->assertFalse($app->hasResource('resource'));
?>
person Bibek Shrestha    schedule 28.03.2010
comment
-1 В вашем втором подходе destroyResource также зависит от createResource, но это не указано явно. Если createResource не работает, UTesting Framework ошибочно укажет, что destroyResource не работает. - person Tivie; 27.11.2013

С вашими тестами действительно есть проблема, если их нужно запускать в определенном порядке. Каждый тест должен быть полностью независимым от других: он помогает локализовать дефект и позволяет получить воспроизводимые (и, следовательно, отлаживаемые) результаты.

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

person jkp    schedule 14.08.2008
comment
PHPUnit поддерживает тестовые зависимости через @depends. - person mjs; 16.12.2009