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

如何测试依赖Service与Repository分层架构的API?

Repository-Service架构的单元测试方案疑问

我有四个采用Repository-Service模式的项目,各层职责如下:

  1. 数据访问层:负责从数据库获取数据;
  2. 仓储层:通过数据访问层对数据库实体执行CRUD操作,通用接口示例:
public interface IEntityBaseRepository<T> where T : class, IEntityBase, new()
{
    IEnumerable<T> AllIncluding(
      params Expression<Func<T, object>>[] includeProperties
    );
    IEnumerable<T> GetAll();
    int Count();
    T GetSingle(int id);
    T GetSingle(Expression<Func<T, bool>> predicate);
    T GetSingle(
      Expression<Func<T, bool>> predicate,
      params Expression<Func<T, object>>[] includeProperties
    );
    IEnumerable<T> FindBy(Expression<Func<T, bool>> predicate);
    T Add(T entity);
    void Update(T entity);
    void Delete(T entity);
    void DeleteWhere(Expression<Func<T, bool>> predicate);
    void Commit();
} 
  1. 服务层:处理应用业务逻辑,依赖仓储层,示例代码:
public class EmployeeService : IEmployeeService
{
    private IEntityBaseRepository<Employee> _employeeRepository;
    private IEntityBaseRepository<User> _userRepository;
    private IEntityBaseRepository<UserRole> _userRoleRepository;
    private IEntityBaseRepository<Role> _roleRepository;
    public EmployeeService(IEntityBaseRepository<Employee> employeeRepository, IEntityBaseRepository<User> userRepository, IEntityBaseRepository<UserRole> userRoleRepository, IEntityBaseRepository<Role> roleRepository)
    {
        _employeeRepository = employeeRepository;
        _userRepository = userRepository;
        _userRoleRepository = userRoleRepository;
        _roleRepository = roleRepository;
    }
    // ... 其他方法省略
    public List<EmployeeServiceModel> GetAllEmployeesForUser(string username)
    {
        var user = _userRepository.GetAll().FirstOrDefault(u => u.UserName == username);
        var userRoles = _userRoleRepository.GetAll().Where(ur => ur.UserId == user.Id);
        var roles = _roleRepository.GetAll();
        var currentRoles = new List<Role>(); 
     
        if (userRoles != null && userRoles.Count() > 0)
        {
            currentRoles = roles.Where(r => userRoles.Any(ur => ur.RoleId == r.Id)).ToList();
        }
        else
        {
            throw new UnauthorizedAccessException(); 
        }
        // ... 后续业务逻辑省略
    }
  1. API层:对外暴露服务,控制器示例:
public class EmployeesController : ControllerBase
{
    private IEmployeeService _employeeService;

    public EmployeesController(IEmployeeService employeeService)
    {
        _employeeService = employeeService;
    }

    // GET: api/<EmployeesController>
    [HttpGet]
    public async Task<IActionResult> Get(string username)
    {
        var employees = _employeeService.GetAllEmployeesForUser(username); 
        return Ok(employees);
    }
}

我的疑问:

  • 该如何对这套架构进行单元测试?
  • 是否只需测试API层?如果是,该如何实现?
  • 项目依赖Service接口,而Service又依赖Repository,我无法直接伪造员工列表让Service的GetAllEmployeesForUser方法返回。
  • 另外,如何测试用户未授权、无员工等场景?
  • 是否只需测试服务层,无需测试API层?

单元测试方案解答

1. 分层测试,而非只测某一层

这套架构的核心是职责分离,单元测试需要对应分层验证:

  • 仓储层:如果只是简单封装数据访问(比如EF Core的DbSet操作),无需单独写单元测试,用集成测试验证它和数据库的交互即可;如果有自定义复杂查询逻辑,再针对这些逻辑写单元测试,此时可以伪造数据访问层的依赖(比如Mock DbContext)。
  • 服务层:这是单元测试的核心。服务层承载了业务逻辑,必须单独测试,因为这部分是业务规则的核心体现。
  • API层:需要测试,但属于控制器测试,重点验证请求路由、参数绑定、返回状态码和响应格式,而非业务逻辑。

2. 服务层单元测试:通过Mock仓储接口实现伪造数据

你可以用Moq、NSubstitute等Mock框架,伪造IEntityBaseRepository<T>的各个实现,控制仓储层的返回结果,进而测试服务层的业务逻辑。

举个测试GetAllEmployeesForUser的例子(用Moq):

[TestClass]
public class EmployeeServiceTests
{
    private Mock<IEntityBaseRepository<User>> _mockUserRepo;
    private Mock<IEntityBaseRepository<UserRole>> _mockUserRoleRepo;
    private Mock<IEntityBaseRepository<Role>> _mockRoleRepo;
    private Mock<IEntityBaseRepository<Employee>> _mockEmployeeRepo;
    private EmployeeService _service;

    [TestInitialize]
    public void Setup()
    {
        // 初始化所有Mock对象
        _mockUserRepo = new Mock<IEntityBaseRepository<User>>();
        _mockUserRoleRepo = new Mock<IEntityBaseRepository<UserRole>>();
        _mockRoleRepo = new Mock<IEntityBaseRepository<Role>>();
        _mockEmployeeRepo = new Mock<IEntityBaseRepository<Employee>>();
        
        // 实例化服务,注入Mock依赖
        _service = new EmployeeService(
            _mockEmployeeRepo.Object,
            _mockUserRepo.Object,
            _mockUserRoleRepo.Object,
            _mockRoleRepo.Object
        );
    }

    // 测试用户未授权场景:用户无角色时抛出异常
    [TestMethod]
    [ExpectedException(typeof(UnauthorizedAccessException))]
    public void GetAllEmployeesForUser_UserHasNoRoles_ThrowsUnauthorized()
    {
        // 1. 准备测试数据:模拟存在用户,但无对应角色关联
        var testUser = new User { Id = 1, UserName = "testUser" };
        _mockUserRepo.Setup(repo => repo.GetAll()).Returns(new List<User> { testUser });
        _mockUserRoleRepo.Setup(repo => repo.GetAll()).Returns(new List<UserRole>());

        // 2. 调用测试方法
        _service.GetAllEmployeesForUser("testUser");
    }

    // 测试正常授权且有员工的场景
    [TestMethod]
    public void GetAllEmployeesForUser_ValidUserWithRoles_ReturnsEmployees()
    {
        // 1. 准备测试数据
        var testUser = new User { Id = 1, UserName = "testUser" };
        var testRole = new Role { Id = 100, Name = "Admin" };
        var testUserRole = new UserRole { UserId = 1, RoleId = 100 };
        var testEmployees = new List<Employee> { new Employee { Id = 200, Name = "John" } };

        // 配置Mock返回
        _mockUserRepo.Setup(repo => repo.GetAll()).Returns(new List<User> { testUser });
        _mockUserRoleRepo.Setup(repo => repo.GetAll()).Returns(new List<UserRole> { testUserRole });
        _mockRoleRepo.Setup(repo => repo.GetAll()).Returns(new List<Role> { testRole });
        _mockEmployeeRepo.Setup(repo => repo.GetAll()).Returns(testEmployees);

        // 2. 调用方法
        var result = _service.GetAllEmployeesForUser("testUser");

        // 3. 验证结果
        Assert.IsNotNull(result);
        Assert.AreEqual(1, result.Count);
        // 可以进一步验证EmployeeServiceModel的属性是否正确映射
    }

    // 测试无员工的场景
    [TestMethod]
    public void GetAllEmployeesForUser_ValidUserButNoEmployees_ReturnsEmptyList()
    {
        // 1. 准备数据:用户有角色,但无员工
        var testUser = new User { Id = 1, UserName = "testUser" };
        var testRole = new Role { Id = 100, Name = "Admin" };
        var testUserRole = new UserRole { UserId = 1, RoleId = 100 };

        _mockUserRepo.Setup(repo => repo.GetAll()).Returns(new List<User> { testUser });
        _mockUserRoleRepo.Setup(repo => repo.GetAll()).Returns(new List<UserRole> { testUserRole });
        _mockRoleRepo.Setup(repo => repo.GetAll()).Returns(new List<Role> { testRole });
        _mockEmployeeRepo.Setup(repo => repo.GetAll()).Returns(new List<Employee>());

        // 2. 调用方法
        var result = _service.GetAllEmployeesForUser("testUser");

        // 3. 验证
        Assert.IsNotNull(result);
        Assert.AreEqual(0, result.Count);
    }
}

3. API层测试:Mock服务接口,验证控制器行为

API层的测试不需要关心业务逻辑,只需要验证:

  • 控制器是否正确调用服务方法
  • 参数是否正确传递
  • 返回的状态码、响应格式是否符合预期

同样用Moq实现示例:

[TestClass]
public class EmployeesControllerTests
{
    private Mock<IEmployeeService> _mockEmployeeService;
    private EmployeesController _controller;

    [TestInitialize]
    public void Setup()
    {
        _mockEmployeeService = new Mock<IEmployeeService>();
        _controller = new EmployeesController(_mockEmployeeService.Object);
    }

    [TestMethod]
    public async Task Get_ValidUsername_ReturnsOkWithEmployees()
    {
        // 1. 准备测试数据
        var testEmployees = new List<EmployeeServiceModel> { new EmployeeServiceModel { Name = "John" } };
        _mockEmployeeService.Setup(svc => svc.GetAllEmployeesForUser("testUser")).Returns(testEmployees);

        // 2. 调用控制器方法
        var result = await _controller.Get("testUser");

        // 3. 验证结果
        var okResult = result as OkObjectResult;
        Assert.IsNotNull(okResult);
        Assert.AreEqual(200, okResult.StatusCode);
        
        var returnedEmployees = okResult.Value as List<EmployeeServiceModel>;
        Assert.AreEqual(1, returnedEmployees.Count);
    }

    [TestMethod]
    public async Task Get_UserUnauthorized_ReturnsUnauthorized()
    {
        // 1. 模拟服务抛出未授权异常
        _mockEmployeeService.Setup(svc => svc.GetAllEmployeesForUser("unauthorizedUser"))
            .Throws(new UnauthorizedAccessException());

        // 2. 调用方法
        var result = await _controller.Get("unauthorizedUser");

        // 3. 验证返回401状态码
        var unauthorizedResult = result as UnauthorizedResult;
        Assert.IsNotNull(unauthorizedResult);
        Assert.AreEqual(401, unauthorizedResult.StatusCode);
    }
}

4. 总结

  • 必须测试服务层:这是业务逻辑的核心,通过Mock仓储层隔离外部依赖,覆盖所有业务场景(授权、无数据、正常数据等)。
  • 建议测试API层:验证控制器的请求处理逻辑,确保接口行为符合预期(状态码、响应格式等)。
  • 仓储层可选测试:如果是简单封装数据访问,用集成测试验证;如果有复杂逻辑,再写单元测试。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 11:01:25