Подтвердить что ты не робот

PHPUnit не продолжает проверку после ожидания исключения

Почему PHPUnit не делает последнее утверждение исключения в этом коде?

public function testConfigOverriding()
{
    $this->dependencyContainer = new DependencyContainer(__DIR__ . "/../../Resources/valid_json.json");
    $this->assertEquals('overriden', $this->dependencyContainer->getConfig('shell_commander')['pygmentize_command']);

    $unexisting = "unexisting_file";
    $this->setExpectedException('Exception', "Configuration file at path \"$unexisting\" doesn't exist.");
    $this->dependencyContainer = new DependencyContainer($unexisting);

    $invalid = __DIR . "/../../Resources/invalid_json.json";
    $this->setExpectedException('Exception', "Configuration JSON file provided is not valid.");
    $this->dependencyContainer = new DependencyContainer($invalid);
}

Итак, в основном: он проверяет, было ли исключено исключение "unexsisting_file", но полностью игнорирует тест "invalid json". Нужно ли мне делать отдельные тесты для каждого из созданных исключений?

4b9b3361

Ответ 1

Даже с setExpectedException ваш тест по-прежнему является регулярным PHP-кодом и следует нормальным нормам PHP. Если генерируется исключение, поток программы немедленно выпрыгивает из текущего контекста, пока не достигнет блока try/catch.

В PHPUnit, когда вы используете setExpectedException, он сообщает ядру PHPUnit, что когда он должен ожидать исключения из кода, который должен запускаться. Поэтому он ждет его с блоком try/catch и передает тест, если вызываемый catch вызывается с ожидаемым типом исключения.

Однако в вашем тестовом методе нормальные правила PHP по-прежнему применяются - когда происходит исключение, это конец текущего блока кода. Ничего больше в этом методе не будет выполнено, если у вас нет собственного блока try/catch в методе тестирования.

Итак, чтобы проверить несколько исключений, у вас есть несколько вариантов:

  • Добавьте свой собственный метод try/catch к методу тестирования, чтобы вы могли продолжить с помощью следующих тестов в этом методе после первого исключения.

  • Разделите тесты на отдельные методы, чтобы каждое исключение находилось в собственном тесте.

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

    /**
     * @dataProvider providerConfigOverriding
     */
    public function testConfigOverriding($filename, $expectedExceptionText) {
        $this->dependencyContainer = new DependencyContainer(__DIR__ . "/../../Resources/valid_json.json");
        $this->assertEquals('overriden', $this->dependencyContainer->getConfig('shell_commander')['pygmentize_command']);
    
        $this->setExpectedException('Exception', $expectedExceptionText);
        $this->dependencyContainer = new DependencyContainer($filename);
    }
    
    public function providerConfigOverriding() {
        return array(
            array('unexisting_file', 'Configuration file at path "unexisting_file" doesn\'t exist.'),
            array(__DIR__ . "/../../Resources/invalid_json.json", "Configuration JSON file provided is not valid."),
        );
    }
    

Надеюсь, что это поможет.

Ответ 2

Я нашел самый простой способ продолжить тестирование после исключения, чтобы реализовать блок try/finally в тесте. Это, по сути, позволяет продолжить выполнение теста независимо от того, какое исключение выбрано.

Это была моя реализация:

$this->expectException(InvalidOperationException::class);

try {
    $report = $service->executeReport($reportId, $jobId);
} finally {
    $this->assertEquals($report->getStatus(), StatusMapper::STATUS_ABORTED);
}

Ответ 3

Во-первых, есть опечатка. Заменить

__DIR

с

__DIR__

:)


Благодаря комментарию @SDC я понял, что вам действительно понадобятся методы проверки spearate для каждого исключения (если вы используете функцию expectedException для PHPUnit). Третье утверждение вашего кода просто не выполняется. Если вам нужно протестировать несколько исключений в одном методе тестирования, я бы рекомендовал написать свои собственные инструкции try catch в методе тестирования.

Еще раз спасибо @SDC

Ответ 4

Для тех, кто хочет делать то, что в заголовке вопроса, это самая чистая вещь, которую я придумал.

$exception_thrown = false

try {
    ... stuff that should throw exception ...
} catch (SomeTypeOfException $e) {
    $exception_thrown = true;
}

$this->assertSame(true, $exception_thrown);

Ответ 5

В ответ на ответ @SDC, я рекомендую следующее

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

Дальнейшее разделение тестов

Проблема с несколькими утверждениями в одном тесте, если утверждения не связаны с одним и тем же поведением: вы не можете правильно назвать тест, вы даже можете использовать and в имени метода тестирования. Если это произойдет, разделите тесты на отдельные тесты

Избегайте использования свойств экземпляра для SUT

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

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

Когда я сталкиваюсь с этим в обзорах кода, мне нравится ссылаться на

и я рекомендую прочитать его.

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

Это также имеет то преимущество, что если вы запускаете тесты с помощью

$ phpunit --testdox

вы получите хороший список ожидаемых действий, см.

Пример, основанный на вашем вопросе

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

/**
 * The name of this method suggests a behaviour we expect from the
 * constructor of DependencyContainer
 */
public function testCanOverrideShellCommanderConfiguration()
{
    $container = new DependencyContainer(__DIR__ . '/../../Resources/valid_json.json');

    $this->assertEquals(
        'overriden', 
        $container->getConfig('shell_commander')['pygmentize_command']
    );
}

/**
 * While the idea of using a data provider is good, splitting the test
 * further makes sense for the following reasons
 *
 * - running tests with --testdox option as lined out above
 * - modifying the behaviour independently 
 *     Currently, a generic Exception is thrown, but you might 
 *     consider using a more specific exception from the SPL library, 
 *     (see http://php.net/manual/en/spl.exceptions.php), 
 *     or creating your own NonExistentConfigurationException class, 
 *     and then a data provider might not make sense anymore)
 */
public function testConstructorRejectsNonExistentConfigurationFile()
{
    $path = 'unexisting_file';

    $this->setExpectedException(\Exception::class, sprintf(
        'Configuration file at path "%s" doesn\'t exist.',
        $path
    ));

    new DependencyContainer($path);
}

public function testConstructorRejectsInvalidConfigurationFile()
{
    $path = __DIR__ . '/../../Resources/invalid_json.json';

    $this->setExpectedException(
        \Exception::class, 
        'Configuration JSON file provided is not valid.'
    );

    new DependencyContainer($path);
}

Примечание Я также рекомендую взглянуть на