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

如何对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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 15:45:00