Допустим, вы поддерживаете код, который использует встроенную в 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