如何对CreateClassroomService模型创建服务进行单元测试?
服务可测性问题分析
你当前的服务并非无法测试,核心问题是Classroom在create方法内部直接通过new关键字实例化,硬编码依赖导致单元测试时无法替换为Mock对象控制save方法的返回值。你可以选择不改代码写集成测试,或者微调结构支持单元测试,两种方案都可行。
方案1:不调整服务代码,编写集成测试
如果不想修改业务实现,直接基于真实数据库环境做集成测试即可,建议使用SQLite内存库+事务回滚避免测试数据污染:
- 测试前置配置好测试数据库连接
- 不需要Mock任何模型,直接创建真实的User测试实例
- 覆盖三个核心测试场景:
- 传入空名称时是否抛出
InvalidArgumentException异常 - 合法参数调用时返回值为
Classroom实例,且name属性、关联的用户信息符合预期 - 数据库中是否成功写入对应班级记录
- 传入空名称时是否抛出
对应测试示例:
public function testGivenEmptyNameWillThrowException(): void { $this->expectException(InvalidArgumentException::class); $user = User::factory()->create(); app(CreateClassroomService::class)->create('', $user); } public function testGivenValidDataWillCreateClassroomSuccess(): void { $name = '测试班级'; $user = User::factory()->create(); $result = app(CreateClassroomService::class)->create($name, $user); // 断言返回值是班级实例 $this->assertInstanceOf(Classroom::class, $result); // 断言属性正确 $this->assertEquals($name, $result->name); $this->assertEquals($user->id, $result->user_id); // 断言数据库存在对应记录 $this->assertDatabaseHas('classrooms', [ 'name' => $name, 'user_id' => $user->id ]); }
方案2:微调服务结构,支持纯单元测试
只需要把Classroom的实例化改成依赖注入的方式,就能实现Mock替换,不需要改动原有业务逻辑,调整后的服务代码:
class CreateClassroomService extends Service { // 注入Classroom原型,用于生成新的实例 public function __construct(private Classroom $classroomPrototype) {} public function create(string $name, User $user): ?Classroom { $this->checkName($name); $classroom = $this->classroomPrototype->newInstance(); $created = $classroom->setName($name) ->associateUser($user) ->save(); return $created ? $classroom : null; } private function checkName(string $name): void { if (empty($name)) { throw new InvalidArgumentException(); } } }
调整后单元测试可以完全不依赖数据库,通过Mock控制所有依赖的返回值:
public function testGivenValidDataWillReturnClassroomInstance(): void { $name = 'Test classroom'; $mockUser = Mockery::mock(User::class); // 模拟Classroom实例,定义方法调用预期 $mockClassroom = Mockery::mock(Classroom::class); $mockClassroom->shouldReceive('setName')->once()->with($name)->andReturnSelf(); $mockClassroom->shouldReceive('associateUser')->once()->with($mockUser)->andReturnSelf(); $mockClassroom->shouldReceive('save')->once()->andReturn(true); // 模拟原型的newInstance方法返回上面的模拟实例 $mockPrototype = Mockery::mock(Classroom::class); $mockPrototype->shouldReceive('newInstance')->once()->andReturn($mockClassroom); $service = new CreateClassroomService($mockPrototype); $result = $service->create($name, $mockUser); // 替换你原来错误的assertTrue,返回值是实例不是布尔值 $this->assertInstanceOf(Classroom::class, $result); $this->assertSame($mockClassroom, $result); } public function testGivenSaveFailWillReturnNull(): void { $mockUser = Mockery::mock(User::class); // 模拟save返回false $mockClassroom = Mockery::mock(Classroom::class); $mockClassroom->shouldReceive('setName')->andReturnSelf(); $mockClassroom->shouldReceive('associateUser')->andReturnSelf(); $mockClassroom->shouldReceive('save')->once()->andReturn(false); $mockPrototype = Mockery::mock(Classroom::class); $mockPrototype->shouldReceive('newInstance')->andReturn($mockClassroom); $service = new CreateClassroomService($mockPrototype); $result = $service->create('测试班级', $mockUser); $this->assertNull($result); }
断言覆盖建议
不管选哪种测试方案,都需要覆盖三个核心校验点:
- 异常场景:空名称输入是否抛出对应参数异常
- 正常流程:调用成功时返回值符合预期,业务逻辑执行顺序正确
- 失败流程:存储失败时返回null,没有异常抛出
内容的提问来源于stack exchange,提问作者longmaikel
相关产品推荐
相关产品推荐

