Статьи

Проверка состояния ошибки с помощью PHPUnit

Допустим, вы поддерживаете код, который использует встроенную в PHP функцию trigger_error() Скажем также, что вы используете PHPUnit для написания модульных тестов для этого кода.

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

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

В чем проблема?

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

Давайте посмотрим на некоторые примеры такого поведения. Во-первых, ошибки.

 <?php
error_reporting(E_ALL | E_STRICT);
echo "Before warningn";
trigger_error("Danger Will Robinson!", E_USER_WARNING);
echo "After warningn";

Вы получите следующий вывод, если запустите приведенный выше код:

  До предупреждения
 Предупреждение PHP: Опасность Уилл Робинсон!  в /home/matt/error_handler.php в строке 4
 После предупреждения 

Отсюда мы видим, что оператор echo после вызова trigger_error()

Теперь исключения.

 <?php
try {
    echo "Before exceptionn";
    throw new Exception("Danger Will Robinson!");
    echo "After exceptionn";
}
catch (Exception $e) {
    echo "In catch blockn";
}

И вывод:

  До исключения
 В блоке catch 

В отличие от примера ошибки, код после исключения был не выполнен. Поскольку PHPUnit преобразует ошибки в исключения, в модульных тестах ошибки ведут себя так же, как и исключения. Любой код, который следует за вызванной ошибкой, не будет выполняться во время тестирования.

Вот еще один пример:

 <?php
function foo($param) {
    if (is_string($param)) {
        trigger_error(__FUNCTION__ . " no longer supports strings, pass an array", E_USER_NOTICE);
    }
    // do useful stuff with $param
    ...
}

С преобразованием ошибки в исключение невозможно проверить, выполняется ли полезная работа с $ param, потому что этот код никогда не будет выполняться, когда ошибка преобразуется в исключение.

Побочные эффекты поведения PHPUnit

Это преобразование ошибки в исключение приводит к отличиям от того, как код будет вести себя при разработке и тестировании, чем от того, как он будет вести себя в производстве. Вот пример:

 <?php
function error_handler($errno, $errstr) {
    throw new Exception($errstr);
}
set_error_handler("error_handler");

try {
    trigger_error("Danger Will Robinson!", E_USER_WARNING);
}
catch (Exception $e) {
    var_dump(error_get_last());
}
restore_error_handler();
trigger_error("Danger Will Robinson!", E_USER_WARNING);
var_dump(error_get_last());

Вот его вывод:

  ЗНАЧЕНИЕ NULL
 Предупреждение PHP: Опасность Уилл Робинсон!  в /home/matt/exception_converter.php в строке 16
 массив (4) {
   [ "Тип"] =>
   Int (512)
   [ "Сообщение"] =>
   Строка (21) "Опасность Уилл Робинсон!"
   [ "Файл"] =>
   string (59) "/home/matt/exception_converter.php"
   [ "Строка"] =>
   INT (14)
 } 

Первый var_dump() Второй var_dump()

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

Решение

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

 <?php
class MyTest extends PHPUnit_Framework_TestCase
{
    private $errors;

    protected function setUp() {
        $this->errors = array();
        set_error_handler(array($this, "errorHandler"));
    }

    public function errorHandler($errno, $errstr, $errfile, $errline, $errcontext) {
        $this->errors[] = compact("errno", "errstr", "errfile",
            "errline", "errcontext");
    }

    public function assertError($errstr, $errno) {
        foreach ($this->errors as $error) {
            if ($error["errstr"] === $errstr
                && $error["errno"] === $errno) {
                return;
            }
        }
        $this->fail("Error with level " . $errno .
            " and message '" . $errstr . "' not found in ", 
            var_export($this->errors, TRUE));
    }

    public function testDoStuff() {
        // execute code that triggers a warning
        $this->assertError("Message for the expected error",
            E_USER_WARNING);
    }
}

setUp() Другие методы, такие как assertError()testDoStuff()

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

Вывод

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

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

Изображение через Fotolia