Laravel Dusk测试中控制器Mock失效,仍进入真实控制器方法
问题
我正在学习Laravel测试并尝试使用Dusk。现有如下CoursesController控制器及对应路由,该路由被VueJS组件调用,我希望通过Dusk进行浏览器测试时Mock控制器的getCoursesCache方法响应。但测试时控制器方法内的断点总是被触发,尝试了两种Mock方式,甚至Mock注入的ApiServiceProvider也无效。预期真实控制器方法不会被执行,请问问题出在哪里?
控制器代码
class CoursesController extends Controller { private ApiServiceProvider $api; public function __construct(ApiServiceProvider $apiServiceProvider) { $this->api = $apiServiceProvider; } public function getCoursesCache(array $cIds = []) : array { // Breakpoint here - always gets hit when running tests if (empty($cIds)) { $cIds = Request::capture()->query('cIds'); $cIds = explode(',', $cIds); } return $this->api->getCoursesCache($cIds); } }
路由代码
Route::get('/api/v1/courses/cache', 'App\Http\Controllers\Api\CoursesController@getCoursesCache')->name('courses.cache');
尝试的Mock代码1
$courseController = $this->mock(CoursesController::class)->makePartial(); $item = new CourseCacheItem(); $item->name = $course->name; $courseController->shouldReceive('getCoursesCache') ->with([$course->getKey()]) ->andReturn([$item]); $this->browse(function (Browser $browser) use ($course) { $browser->visit('/') ->waitFor('.course-card-container--data-loaded', 10) ->screenshot('filename') ->assertSee($course->name); });
尝试的Mock代码2
$cc = $this->createMock(CoursesController::class); $item = new CourseCacheItem(); $item->name = $course->name; $cc->expects($this->once())->method('getCoursesCache')->with([$course->getKey()])->willReturn([$item]);
分析与解决
核心问题:Dusk的运行机制导致Mock失效
Dusk是浏览器端测试,它会发起真实的HTTP请求到你的应用,和单元测试的运行环境完全隔离:
- 你在测试代码里创建的Mock对象,只存在于测试进程中
- 处理浏览器请求的是另一个独立的应用进程,根本看不到你创建的Mock,所以还是会实例化真实的
CoursesController并执行方法
正确的Mock方式
你需要针对应用容器绑定Mock,让应用进程在解析CoursesController或其依赖时拿到Mock实例,而不是在测试进程里创建Mock后不管:
方案1:Mock控制器的依赖ApiServiceProvider
既然控制器的getCoursesCache最终调用的是$this->api->getCoursesCache,直接Mock这个依赖更简单,而且能避免控制器逻辑被执行:
$this->mock(ApiServiceProvider::class, function ($mock) use ($course) { $item = new CourseCacheItem(); $item->name = $course->name; $mock->shouldReceive('getCoursesCache') ->with([$course->getKey()]) ->andReturn([$item]); }); $this->browse(function (Browser $browser) use ($course) { $browser->visit('/') ->waitFor('.course-card-container--data-loaded', 10) ->assertSee($course->name); });
通过$this->mock()方法,Laravel会自动把Mock实例绑定到服务容器中,应用进程在解析控制器依赖时会拿到这个Mock,从而跳过真实的API调用。
方案2:绑定Mock控制器到容器
如果你一定要Mock控制器本身,需要把Mock实例绑定到容器,替换掉真实的控制器:
$mockController = $this->mock(CoursesController::class); $item = new CourseCacheItem(); $item->name = $course->name; $mockController->shouldReceive('getCoursesCache') ->with([$course->getKey()]) ->andReturn([$item]); // 把Mock绑定到容器,让应用解析时用这个实例 $this->app->instance(CoursesController::class, $mockController); $this->browse(function (Browser $browser) use ($course) { $browser->visit('/') ->waitFor('.course-card-container--data-loaded', 10) ->assertSee($course->name); });
注意:这种方式需要确保控制器是通过容器解析的(Laravel默认路由解析控制器时会用容器,所以没问题)。
为什么之前的Mock无效?
- 你创建的Mock只是测试进程中的一个对象,没有告诉Laravel的服务容器在解析控制器时使用它
- Dusk的浏览器请求会触发新的应用实例,完全独立于测试代码的上下文,所以测试代码里的Mock对它毫无作用
内容的提问来源于stack exchange,提问作者Chris Rockwell
相关产品推荐
相关产品推荐

