如何在 phpunit 中使用 error_get_last() 函数模拟和正确编写测试

How to simulate and properly write test with error_get_last() function in phpunit

所以问题与主题相同: 如何在 phpunit

中使用 error_get_last() 函数模拟和正确编写测试

用于编写测试的示例代码:

    $lasterror = error_get_last();
    if ($lasterror !== null) {
        //do something
    }

感谢您提出这个问题,一开始我并不清楚问题是由于为其编写测试而导致的。这是由于错误和异常处理的性质,尤其是在测试这些东西时,可以认为这些东西有点难,而且通常没有现成的“简单”解决方案,因为最终你需要自己编写测试。

我希望这可以在评论中得到澄清,下面可以展示如何以某种方式处理它们。

NOTE: As we're writing test-code, I use assert() statements in the examples, and this is by intention as assert is a more pure language feature. It makes the examples more portable, too.

To make use of it, execute php having assertions in development mode and throwning (zend.assertions=1 and assert.exception=1) and run the example as it evolves and have later phpunit run with these settings as well.

You can certainly port this into a concrete test-case to implement it Phpunit specific, for the examples the concrete assertion library should not be of interest, but the assertions. They are expected in the examples to bring PHP to halt if they aren't fullfilled and this is how I wrote the examples (some people say those are expectations).


在测试出现错误的副作用的一般测试代码中:

    $lasterror = error_get_last();
    if ($lasterror !== null) {
        //do something
    }

然后在你的测试套件中你得到的不是错误而是异常:

@trigger_error('Oh no'); // ErrorException: Oh no

这将阻止进一步的代码到 运行(您要测试的代码)。

现在怎么办?嗯,赤裸裸的错误触发器会让 phpunit 突出显示你有一个 ErrorException,你可以做的是断言 fixture-setup 是正确的(fixture 是“哦不”错误)并且只是这样声明它。

什么?是的,我们就是这么说,把我们面前的障碍变成了好的状态:

try {
    @trigger_error('Oh no');
} catch (ErrorException $good) {
    assert('Oh no' === $good->getMessage(), get_class($good));
} finally {
    assert(isset($good), 'an expected exception was thrown');
}

这个例子有助于更好地描述正在发生的事情以及我们知道要处理的事情,但是最初的问题仍然存在:由于一些错误处理程序将错误转化为异常,error_get_last() returns NULL因为没有错误,但是有异常。

执行这段代码。它会显示它通过了。

所以断言是相反的。让我们这样做,通过否定它把好的条件变成这个坏的、坏的失败:

try {
    @trigger_error('Oh no');
} catch (ErrorException $fail) {
    assert(false, sprintf('the "%s" exception must not be thrown', get_class($fail)));
} finally {
    assert(!isset($fail), 'there must not have been any exception');
}

希望你能跟到这里。第一次翻转。

现在只有在给出预期状态时才会断言夹具已正确设置,这里断言没有抛出 ErrorException,因为我们了解到这将是“糟糕的”(破坏任何代码测试 error_get_last()打电话)。

这通常是人们期望的第一个版本。我们现在实际上已经做了两个测试 运行s。一绿一红

只要这段代码没有通过,我们就知道夹具没有设置,因此甚至不需要 运行 任何进一步的测试。

如您所见,测试与编程一样简单。你编写测试代码就好像一切都已经安排妥当,纯粹是魔术。即使它根本没有意义(还没有测试,对吧?!),这仍然是您想要实现的目标。

为什么这么简单的事情这么难?一方面可能是在测试之前编写代码在编写测试时可能会很烦人。满脑子都是对代码的期待,而应该只专注于测试(-code)。

别着急,只是不要因为测试首先不起作用而生气,否则也一样。一个好的测试从一开始就很快失败开始。这正是我们所处的位置,我们所做的只是将条件红色变为绿色。第二次翻转。


In testing this is also called the Happy Path, that is where it goes happily ever after and the test just tests for that what when things are unspecifically just working and unspecifically just testing for it.

But this only to side-track you a bit.


所以现在让夹具通过这个快乐路径(删除现有的错误处理程序,设置错误,恢复旧的处理程序,我知道,你已经从手册中阅读了这个,对吧?所以我可以保留链接,抱歉偷懒):

try {
    set_error_handler(null);   //   <--- this
    @trigger_error('Oh no');
    restore_error_handler();   //   <--- restore original runtime configuration
} catch (ErrorException $fail) {
    assert(false, sprintf('the "%s" exception must not be thrown', get_class($fail)));
} finally {
    assert(!isset($fail), 'there must not have been any exception');
}

Et voilá,幸福的道路是绿色的!

但这只是幸福的道路。好的测试也会测试其他路径。并且严谨。

我们没有吗?对,是的,第一个断言 ErrorException 被触发的那个,我们已经有了那个测试。尽管失败了,但曾经很好的东西现在又很好了(但尽管有名字,但在不愉快的道路上)。第三次翻转。

try {
    set_error_handler(null);
    @trigger_error('Oh no');
    restore_error_handler();
    try {
        @trigger_error('Oh no');
    } catch (ErrorException $good) {
        assert('Oh no' === $good->getMessage(), get_class($good));
    } finally {
        assert(isset($good), 'an expected exception was thrown');
    }
} catch (ErrorException $fail) {
    assert(false, sprintf('the "%s" exception must not be thrown', get_class($fail)));
} finally {
    assert(!isset($fail), 'there must not have been any exception');
}

哇,现在这肯定随着故事的发展而升级。它甚至还没有完成。您已经发现了它的一个或另一个问题,对吗?

缺少在快乐路径上存在 ErrorException 抛出错误处理程序的初始期望。让我们把它放在顶部作为早期检查(然后取消设置检查变量,因为 post-assertion 有重复使用)。

更重要的是,确保 error_get_last() returns NULL 否则测试很容易被结果污染:

try {
    @trigger_error('Oh no');
} catch (ErrorException $good) {
    assert('Oh no' === $good->getMessage(), get_class($good));
} finally {
    assert(isset($good), 'an expected exception was thrown');
}
unset($good);
assert(NULL === error_get_last());  // <-- this easy to miss

try {
    set_error_handler(null);
    @trigger_error('Oh no');
    restore_error_handler();
    try {
        @trigger_error('Oh no');
    } catch (ErrorException $good) {
        assert('Oh no' === $good->getMessage(), get_class($good));
    } finally {
        assert(isset($good), 'an expected exception was thrown');
    }
} catch (ErrorException $fail) {
    assert(false, sprintf('the "%s" exception must not be thrown', get_class($fail)));
} finally {
    assert(!isset($fail), 'there must not have been any exception');
}

// continue here...

它很冗长,但显示了所有已完成的事情:

  • 意识到单独触发错误是行不通的,为什么
  • 前提是error_get_last(); returns设置错误前为NULL
  • 有一个 ErrorException 错误处理程序需要让开以触发真正的 PHP 错误
  • 重新设置此错误处理程序,因为被测系统可能需要它(或应该用它进行测试)

由于 assert() 声明,它记录了自己的期望。

将它包装在一个函数中,并给它起一个有用的名称,并对其用途进行简短描述。你可以——但不能——保护断言。我通常在 Phpunit 测试套件中这样做,这样预期的 运行 时间配置就不会丢失。

到此结束,希望对你来说是另一个开始。

这只是解决此类难题的一种方法,而且这些问题真的很难测试,因为它充满了副作用。 error_get_last() 不仅会对直接处理造成问题。

是的,错误处理很难,经常被遗忘或以“用火烧掉它”的态度攻击。归根结底,当您实际需要处理它时,这并没有帮助。

根据它的定义,为此编写测试是。


完整示例,包括。模拟错误异常抛出错误处理程序和 3v4l.org 上的一些早期翻转:https://3v4l.org/8fopq


It should go without saying that turning all errors into exceptions is some of the stupidest idea some developers have fallen for. It is just another example that error handling is hard.

Errors and exceptions are a major source of complexity and bugs

If you want get know something talk with the another human.

非常感谢@hakre!

并压缩以上答案:

protected function generateSafelySimulatedError(\Closure $errorGeneratingFn, string $generatedErrorMsg = null): void
{
    try {
        set_error_handler(null);
        set_exception_handler(null);
        $errorGeneratingFn();
        restore_error_handler();
        restore_exception_handler();
        try {
            $errorGeneratingFn();
        } catch (\ErrorException $good) {
            if ($generatedErrorMsg !== null) {
                $this->assertTrue($generatedErrorMsg === $good->getMessage(), get_class($good));
            }
        } finally {
            $this->assertTrue(isset($good), 'An expected exception was thrown.');
        }
    } catch (\ErrorException $fail) {
        $this->assertTrue(false, sprintf('The "%s" exception must not be thrown.', get_class($fail)));
    } finally {
        $this->assertTrue(!isset($fail), 'There must not have been any exception.');
    }
}

和示例用法:

public function testCriticalErrors(): void
{
    $testObject = $this->getObjectUnderTest();//Here is creation of object and also here is set_error_handler() and other similar.

    $this->generateSafelySimulatedError(function (): void {
        @trigger_error('Oh no');
    }, 'Oh no');

    $testObject->criticalErrors();//tested method like:
    /*
    $lasterror = error_get_last();
    if ($lasterror !== null) {
        //do something
    }
    */
    //Below other assertions if needed.
    $this->expectOutputString('<html class="error"><body><h1>500</h1>Error 500</body></html>');
}