如何模拟无法传递给方法以触发异常的对象?

How do I mock an object I can't pass to a method to trigger an exception?

考虑以下方法:

function m1()
{
    $ent = new Entity;
    ...
    try {
        $ent->save();
    } catch (QueryException $e) {
        ...
    }

我必须触发异常。最好用 mockery。我该怎么做?

P.S。我无法将 $ent 传递到方法中。

UPD 让我描述一下我的特殊情况,以确认我是否确实需要触发异常。在这里,我试图测试由支付系统触发的控制器操作,以通知用户已付款。在其中,我在数据库中存储了来自 PaymentSystemCallback 模型中支付系统的所有数据,并将其 link 存储到 Order 模型中,该模型是在将用户重定向到支付之前创建的系统。所以,它是这样的:

function callback(Request $request)
{
    $c = new PaymentSystemCallback;
    $c->remote_addr = $request->ip();
    $c->post_data = ...;
    $c->headers = ...;
    ...
    $c->save();

    $c->order_id = $request->request->get('order_id');
    $c->save();
}

但是如果不正确的order_id进来了,外部约束就失效了,所以我改成这样:

try {
    $c->save();
} catch (QueryException $e) {
    return response('', 400);
}

但是以这种方式处理任何数据库异常看起来都不太好,所以我正在寻找一种重新抛出异常的方法,除非 $e->errorInfo[1] == 1452.

您的方法不是为测试而设计的。解决这个问题。如果你不能,那么你必须打猴子补丁,PHP does not support natively

我推荐的方法是让您的测试套件安装自己的优先级自动加载器。让您的测试用例将模拟 class 注册到该自动加载器中,并与 class 名称 Entity 相关联。您的模拟 class 将施展魔法抛出异常。如果您使用的是 PHP 7,您可以访问匿名的 classes,这使得 fixtures 更容易:new class Entity {}.

根据已接受的答案,Mockery 在模拟的 classes 上使用 overload: 量词支持这种自动加载技巧。这为您节省了大量工作!

解决此问题的最简单方法是调用一个工厂方法来创建实体的模拟实例。类似于:

function testSomething()
{
    $ent = $this->getEntity();
    ...
    try {
        $ent->save();
    } catch (QueryException $e) {
        ...
    }
}

function getEntity()
{
    $mock = $this->createMock(Entity::class);
    $mock
        ->method('save')
        ->will($this->throwException(new QueryException));

    return $mock;
}

这是我想出的:

/**
 * @runInSeparateProcess
 * @preserveGlobalState disabled
 */
function testExceptionOnSave()
{
    $this->setUpState();

    Mockery::mock('overload:App\PaymentSystemCallback')
        ->shouldReceive('save')
        ->andReturnUsing(function() {}, function() {
            throw new QueryException('', [], new Exception);
        });

    $this->doRequest();

    $this->assertBalanceDidntChange();
    $this->assertNotProcessed();
    $this->seeStatusCode(500);
}

我使用 @runInSeparateProcess 因为之前的测试会触发相同的操作,因此 class 在 mockery 有机会模拟它之前加载。

至于@preserveGlobalState disabled没有它就不行。正如 phpunit's documentation 所说:

Note: By default, PHPUnit will attempt to preserve the global state from the parent process by serializing all globals in the parent process and unserializing them in the child process. This can cause problems if the parent process contains globals that are not serializable. See the section called “@preserveGlobalState” for information on how to fix this.

当我在一个单独的过程中只将一项测试标记为 运行 时,我与 mockery's documentation 所说的略有不同,因为我只需要一次测试。不是整个 class.

欢迎批评。