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
相关产品推荐
相关产品推荐

