Avatar ·

Как в PHPUnit проверить, что метод выбросил исключение?

Всем привет! Я только начал осваивать PHPUnit и пишу тесты для своего проекта. У меня есть метод, который при определённых условиях должен выбрасывать исключение, например, `InvalidArgumentException`, если передан неверный аргумент. Проблема в том, что я не могу понять, как правильно написать assert, который проверяет, что исключение было выброшено. Я пробовал оборачивать вызов метода в `try...catch` и потом делать `$this->assertTrue($caught)`, но это выглядит коряво и не использует возможности PHPUnit. Вот пример моего кода: ```php class Calculator { public function divide($a, $b) { if ($b == 0) { throw new \InvalidArgumentException('Division by zero.'); } return $a / $b; } } ``` И мой тест, который некрасиво проверяет исключение: ```php public function testDivisionByZero() { $calculator = new Calculator(); $caught = false; try { $calculator->divide(10, 0); } catch (\InvalidArgumentException $e) { $caught = true; } $this->assertTrue($caught); } ``` Есть ли в PHPUnit встроенный метод для такой проверки? Как правильно написать тест, чтобы он был лаконичным и читаемым? Заранее спасибо за помощь! <<>>

Использование встроенных методов PHPUnit для проверки исключений

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

1. Метод expectException() (рекомендуемый)

Это самый популярный и читаемый способ. Вы просто вызываете $this->expectException() до того, как выполните код, который должен выбросить исключение:

public function testDivisionByZero() {
    $calculator = new Calculator();

    // Говорим PHPUnit, что ожидаем исключение
    $this->expectException(\InvalidArgumentException::class);

    // Выполняем код, который должен выбросить исключение
    $calculator->divide(10, 0);
}

Если исключение не будет выброшено, тест упадёт с ошибкой "Failed asserting that exception of type "InvalidArgumentException" is thrown". Если исключение другого типа — тоже упадёт.

2. Проверка сообщения и кода исключения

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

public function testDivisionByZeroWithMessage() {
    $calculator = new Calculator();

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('Division by zero.'); // точное совпадение
    // или $this->expectExceptionMessageMatches('/division/i'); // по регулярке

    $calculator->divide(10, 0);
}

Также можно проверить код исключения с помощью expectExceptionCode():

$this->expectExceptionCode(100); // если у исключения есть код

3. Атрибут #[ExpectException] (PHPUnit 10+)

В новых версиях PHPUnit (начиная с 10) появилась возможность использовать атрибуты. Это более декларативный стиль:

use PHPUnit\Framework\Attributes\ExpectException;

class CalculatorTest extends TestCase {
    #[ExpectException(\InvalidArgumentException::class)]
    public function testDivisionByZero() {
        $calculator = new Calculator();
        $calculator->divide(10, 0);
    }
}

Можно комбинировать с другими атрибутами:

#[ExpectException(\InvalidArgumentException::class)]
#[ExpectExceptionMessage('Division by zero.')]
public function testDivisionByZero() {
    // ...
}

Важные моменты

  • Порядок важен: методы expectException* должны вызываться до того, как будет выполнен код, выбрасывающий исключение.
  • Одно исключение на тест: старайтесь проверять только одно исключение в одном тесте. Если нужно проверить несколько сценариев — пишите отдельные тесты.
  • Не ловите исключение вручную: если вы напишете try...catch внутри теста с expectException, PHPUnit может не заметить, что исключение было выброшено.

Используя эти методы, ваш тест станет лаконичным, читаемым и будет следовать best practices PHPUnit.

<<>>

Альтернативный подход: Data Provider для множественных сценариев

Если у вас есть несколько случаев, когда метод должен выбрасывать исключение, удобно использовать @dataProvider вместе с expectException(). Это позволяет избежать дублирования кода:

class CalculatorTest extends TestCase {
    /**
     * @dataProvider divisionByZeroProvider
     */
    public function testDivisionByZero($a, $b) {
        $calculator = new Calculator();
        $this->expectException(\InvalidArgumentException::class);
        $calculator->divide($a, $b);
    }

    public function divisionByZeroProvider(): array {
        return [
            [10, 0],
            [0, 0],
            [-5, 0],
        ];
    }
}

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

Также для старых версий PHPUnit (до 6.0) можно использовать аннотацию @expectedException в docblock, но это устаревший синтаксис, и его не рекомендуется применять в новых проектах:

/**
 * @expectedException \InvalidArgumentException
 */
public function testDivisionByZero() {
    // ...
}

Советую переходить на современные методы — expectException() или атрибуты, так как они лучше поддерживаются IDE и статическими анализаторами.