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

Laravel9测试中自定义Exception无法被expectException捕获的问题

解决Laravel 9中自定义异常测试失败的问题

问题背景

在Laravel 9项目中编写功能测试时,尝试捕获自定义UserAccountManagerAccessException异常,使用了InteractsWithExceptionHandling特性和withoutExceptionHandling()方法,但测试执行失败。已确认异常被抛出,但expectException和assertStatus断言均未生效。

测试代码

<?php

namespace Tests\Feature;

//use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;

use Tests\TestCase;
use Carbon\Carbon;
use App\Models\Article;
use App\Models\User;
use Illuminate\Support\Str;

class ArticlesCrudTest extends TestCase
{
    use InteractsWithExceptionHandling;

    public function testNegativeArticleGotFailureWithInactiveUserToBeAdded()
    {
        $loggedUser = clone(self::$loggedUser);


        $this->withoutExceptionHandling();


        $loggedUser->status = 'I' ; // Inactive
        $newArticleObject = \App\Models\Article::factory(Article::class)->make();

        $response = $this->actingAs($loggedUser, 'api')->post(route('articles.store'),
            $newArticleObject->toArray());

        $this->expectException(\App\Exceptions\UserAccountManagerAccessException::class);

        $response->assertStatus(400);
    }
}

测试错误输出

1) Tests\Feature\ArticlesCrudTest::testNegativeArticleGotFailureWithInactiveUserToBeAdded
App\Exceptions\UserAccountManagerAccessException: Your account must be active in "store" !

/ProjectPath/app/Library/Services/LocalUserAccountManager.php:59
/ProjectPath/app/Repositories/ArticleCrudRepository.php:191
/ProjectPath/app/Http/Controllers/ArticleController.php:86
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Controller.php:54
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php:43
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Route.php:260
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Route.php:205
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Router.php:725
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:141
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php:50
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php:126
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php:102
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php:54
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php:44
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:116
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Router.php:726
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Router.php:703
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Router.php:667
/ProjectPath/vendor/laravel/framework/src/Illuminate/Routing/Router.php:656
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:190
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:141
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php:21
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php:31
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php:21
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php:40
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php:27
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php:86
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php:62
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php:39
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
/ProjectPath/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:116
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:165
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:134
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:545
/ProjectPath/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:340
/ProjectPath/tests/Feature/ArticlesCrudTest.php:108

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

相关代码细节

routes/api.php

Route::middleware('auth:api')->group(function () {
    Route::apiResource('articles', ArticleController::class);
});

app/Http/Controllers/ArticleController.php

public function store(Request $request)
{
    return $this->articleCrudRepository->store(data: $request->only('title', 'text', 'text_shortly',
        'creator_id', 'published'), makeValidation: true);
}

app/Repositories/ArticleCrudRepository.php

<?php
namespace App\Repositories;

...
class ArticleCrudRepository 
{
    public function store(array $data, bool $makeValidation = false): JsonResponse|MessageBag
    {
        ...    
         $this->userAccountManager->checkPermissions( [ ], "store");
        // UserAccountManagerAccessException is raised in method above, BEFORE try block with transaction...
        
        DB::beginTransaction();
        try {
            $article = Article::create([
                'title'        => $data['title'],
                'text'         => $data['text'],
                'text_shortly' => $data['text_shortly'],
                'creator_id'   => $data['creator_id'],
                'published'    => $data['published'],
            ]);
            DB::Commit();

            $article->load('creator');

            return response()->json(['article' => (new ArticleResource($article))],  201); 
        } catch (\Exception $e) {
            $errorMessage = $e->getMessage();
            \Log::info($errorMessage);
            DB::rollback();

            return sendErrorResponse($e->getMessage(), 500);
        }
    }
}

项目依赖

"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^9.19",
"laravel/passport": "^11.3"

解决方案

1. 调整测试代码顺序

expectException必须在发送请求之前声明,因为请求执行时异常会立即抛出,后续代码无法执行。修改后的测试方法:

public function testNegativeArticleGotFailureWithInactiveUserToBeAdded()
{
    $loggedUser = clone(self::$loggedUser);
    $loggedUser->status = 'I'; // Inactive
    $newArticleObject = \App\Models\Article::factory(Article::class)->make();

    $this->withoutExceptionHandling();
    $this->expectException(\App\Exceptions\UserAccountManagerAccessException::class);

    $response = $this->actingAs($loggedUser, 'api')->post(route('articles.store'),
        $newArticleObject->toArray());
}

2. 自定义异常类添加HTTP状态码处理

如果需要断言响应状态码,需在自定义异常类中定义render方法,指定对应的HTTP状态码:

namespace App\Exceptions;

use Exception;
use Symfony\Component\HttpFoundation\Response;

class UserAccountManagerAccessException extends Exception
{
    public function render($request)
    {
        return response()->json([
            'message' => $this->getMessage()
        ], Response::HTTP_BAD_REQUEST); // 对应400状态码
    }
}

3. 可选:使用assertThrows简化测试(Laravel 9+支持)

Laravel 9及以上版本支持assertThrows方法,可更直观地测试异常:

public function testNegativeArticleGotFailureWithInactiveUserToBeAdded()
{
    $loggedUser = clone(self::$loggedUser);
    $loggedUser->status = 'I'; // Inactive
    $newArticleObject = \App\Models\Article::factory(Article::class)->make();

    $this->actingAs($loggedUser, 'api');
    
    $this->assertThrows(function () use ($newArticleObject) {
        $this->post(route('articles.store'), $newArticleObject->toArray());
    }, \App\Exceptions\UserAccountManagerAccessException::class);
}

关键说明

  • withoutExceptionHandling()会关闭Laravel的全局异常捕获转换,让异常直接抛出到测试环境,确保expectException能捕获到。
  • 所有测试预期设置(如expectException)必须放在触发异常的代码(发送请求)之前,否则异常抛出后后续代码不会执行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 09:20:42