You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel+Pest测试:如何断言已被业务代码捕获的异常?

问题场景

我用Laravel+Pest做测试,想断言方法执行时会抛出指定异常,但测试失败。业务代码里的try-catch已经捕获了异常,只做日志记录和事务回滚,没重新抛出。

业务代码

核心处理方法

public static function stuff(): void
{
        DB::beginTransaction();

        try {
            [$width, $height, $type] = $this->getImageSize($path);

            $model->update([
                'width' => $width,
                'height' => $height,
                'type' => image_type_to_mime_type($type),
            ]);

            DB::commit();
        } catch (Exception $e) {
            Log::error($e->getMessage());
            DB::rollBack();
        }
}

抛出异常的私有方法

private function getImageSize($path): ?array
{
    if (! Storage::disk('images_admin')->exists($path)) {
        throw new Exception("Image not found: $path");
    }

    $file = Storage::disk('images_admin')->path($path);
    $data = getimagesize($file);

    return [
        $data[0],
        $data[1],
        $data[2],
    ];
}

测试代码

it('should not fill width, height and type of image if path is not found', function () {
    $model = Image::factory()->create([
        'path' => 'images/image.jpg',
    ]);

    Log::spy();
    
    expect(SaveImageMetadata::run(fn () => $model, 'images/not-found.jpg'))->toThrow(Exception::class);
    //    SaveImageMetadata::run($model, 'images/not-found.jpg');

    Log::shouldHaveReceived('error')
    ->withArgs(function ($message) {
        return $message === "Image not found: images/not-found.jpg";
    })
    ->once();

    expect($model->width)->toBeNull();
    expect($model->height)->toBeNull();
    expect($model->type)->toBeNull();
})

测试报错

Exception "Exception" not thrown.

需求:不修改业务代码的前提下,在Pest测试里验证这个被捕获的异常确实被抛出过。


解决方案

方法1:通过验证最终业务行为间接证明异常触发

既然业务代码捕获异常后会触发「记录错误日志」「回滚事务(模型无更新)」这两个确定行为,直接验证这两个结果就能反向证明异常发生了,这也是测试的核心目的——验证业务逻辑输出是否符合预期:

  • 保留日志断言,确认错误信息被正确记录
  • 保留模型字段断言,确认模型未被更新
  • 移除->toThrow()断言,因为异常确实不会向外抛出

调整后的测试代码:

it('should not fill width, height and type of image if path is not found', function () {
    $model = Image::factory()->create([
        'path' => 'images/image.jpg',
    ]);

    Log::spy();
    
    SaveImageMetadata::run($model, 'images/not-found.jpg'); // 直接执行方法,无需用expect包裹

    // 验证错误日志已记录
    Log::shouldHaveReceived('error')
        ->withArgs(fn($message) => $message === "Image not found: images/not-found.jpg")
        ->once();

    // 从数据库重新拉取模型,确保拿到最新状态
    $model->refresh();
    // 验证模型未被更新
    expect($model->width)->toBeNull();
    expect($model->height)->toBeNull();
    expect($model->type)->toBeNull();
})

方法2:Mock私有方法,直接监听异常抛出动作

如果一定要验证getImageSize确实抛出了异常,可以用Mockery监听这个私有方法的行为:

it('should throw exception when image path is not found', function () {
    $model = Image::factory()->create([
        'path' => 'images/image.jpg',
    ]);

    // 创建SaveImageMetadata类的局部Mock,监听getImageSize方法
    $service = Mockery::mock(SaveImageMetadata::class)->makePartial();
    $service->shouldReceive('getImageSize')
        ->with('images/not-found.jpg')
        ->andThrow(new Exception("Image not found: images/not-found.jpg"));

    // 模拟静态run方法调用核心处理逻辑
    SaveImageMetadata::shouldReceive('run')
        ->once()
        ->andReturnUsing(function () use ($service) {
            $service->stuff();
        });

    // 执行方法
    SaveImageMetadata::run($model, 'images/not-found.jpg');

    // 验证日志记录行为
    Log::shouldHaveReceived('error')
        ->withArgs(fn($message) => $message === "Image not found: images/not-found.jpg")
        ->once();
})

方法3:临时注册全局异常监听器

在测试中临时替换Laravel的异常处理器,捕获所有抛出的异常:

it('should catch the image not found exception', function () {
    $caughtException = null;

    // 临时注册自定义异常处理器,捕获抛出的异常
    app()->instance(ExceptionHandler::class, new class extends ExceptionHandler {
        public function report(Throwable $e)
        {
            global $caughtException;
            $caughtException = $e;
            parent::report($e);
        }

        public function render($request, Throwable $e)
        {
            return parent::render($request, $e);
        }
    });

    $model = Image::factory()->create([
        'path' => 'images/image.jpg',
    ]);

    SaveImageMetadata::run($model, 'images/not-found.jpg');

    // 验证捕获到的异常信息
    expect($caughtException)->toBeInstanceOf(Exception::class);
    expect($caughtException->getMessage())->toBe("Image not found: images/not-found.jpg");

    // 恢复默认异常处理器
    app()->instance(ExceptionHandler::class, app(\App\Exceptions\Handler::class));
})

总结

最推荐方法1,它聚焦业务逻辑的最终结果,符合测试的核心目标;如果必须验证异常抛出的中间动作,方法2的Mock方案更简洁且无需修改业务代码。

内容的提问来源于stack exchange,提问作者Cordellito

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 07:23:20