只有当给定的代码块对于集成测试来说太复杂时,才应该对它使用单元测试吗?

Should one use unit tests for a given code block only when it's too complicated for Integration tests?

我们很久以来就有这个问题,而且经常不时地浮出水面,争论不休。在我们开发的系统中,我们习惯于在不同层(Workers 和 Controllers/Presenters)创建单元测试来模拟后续层,同时我们在演示者/控制器级别创建集成测试。

设想以下场景,

存储库

UserRepository
{
      User GetByUserName(string username);
}

OrderRepository
{
       List<Order> FindForUser(int userId);
}

工人

UserOrderWorker
{
        //constructor injected
        UserRepository _userRepo;
        OrderRepository _orderRepo;

       IList<Order> FindOrders(string userName)
       {
                var user = _userRepo.GetByUserName(userName);
                return _orderRepo.FindForUser(user.Id);        
       }
}

控制器

UserOrderController
{
       View _view;
       //constructor injected
       UserOrderWorker _worker;

       void Index()
       {
                _view.Orders = _worker. FindOrders(_view.UserName);
       }
}

如果我们要通过模拟 UserRepository 和 OrderRepository 来为 worker 创建单元测试。我们还通过模拟视图和 UserOrderController 为 UserOrderController 创建单元测试。因此,为了涵盖这一点,我们需要 3 个单元测试,还需要 2 个针对存储库的集成测试。所以一天结束时总共进行了 5 次测试。

另一方面,如果我们要为此创建集成测试,我们只需要为 UserOrderController 创建一个。

对于仅使用集成测试的参数,

  1. 如果考虑到应用程序的实际使用情况,用户将只使用控制器,集成测试将涵盖最终用户的场景。

  2. 如果在中间层进行了更改(例如,如果通过将存储库方法 FindForUser(int userId) 更改为 FindForUser(int userId,List<Status> statuses)) 来仅获取具有特定状态的订单,则将是最小的需要在集成测试中进行更改(在接受状态之外添加额外的订单),其中大部分测试需要在单元测试中更改(2 个测试需要更改,需要添加更多测试。

对于仅使用单元测试的参数,

  1. 你可以更快地指出失败——然而,大多数时候失败是由于方法签名的改变(因为有人忘记配置模拟对象)

  2. 更好的文档——好的集成测试文档应该足以识别客户需求,在我看来这已经足够好了。

考虑到以上所有内容,我不确定为什么我们实际上需要单元测试,而我们实际上可以通过使用集成测试获得更好的结果。 (当然有人可能会说集成测试很昂贵,但我会说它并不昂贵,因为当业务需求发生变化时,开发人员花在修改单元测试上的时间)

你的例子太简单了,不具有代表性。该代码中没有复杂的逻辑。当你有很多 ifs 和循环时,它就会改变。

假设您的应用程序中有 n if 个语句。这意味着您有 2^n 个可能的不同执行流程。所以你可以用 2 个单元测试来测试每个 if,因此你有 2n 个单元测试。或者您只能进行集成测试。但是你需要 2^n 次测试。

循环使问题更加复杂。因此,当您没有逻辑 (getters/setters/sequences) 时,您可能根本不需要任何测试。但是当你有复杂的逻辑时,也许最好单独测试那个复杂的东西

另一件事是集成测试的难度。例如,您想测试仅在年底运行的代码。在单元测试中准备环境(如时间)比在集成测试中更容易,在集成测试中你不应该过多地干扰系统的内部

单元测试和集成测试有不同的用途:

  • 通过单元测试,您可以验证给定组件 是否按预期工作
  • 通过集成测试,您可以验证一组组件是否 协同工作,而无需验证特定组件的详细信息

您的案例非常微不足道,单元测试可能无法为 UserOrderController 提供超过一个体面的集成测试。但这只是因为你的类非常小,不包含复杂的逻辑。

但是,当 UserOrderWorker 变得更复杂时会发生什么?

  • 如果 _userRepo returns null 因为没有找到用户怎么办?
  • 如果 _userRepo 因连接数据库超时而抛出异常怎么办?
  • 如果引入新要求说对于特定用户 (parents) 您还需要提取其 children 订单怎么办?

要使用集成测试来测试此类场景,您必须对每个特定案例进行单独测试。乘以参与集成测试的组件数。这通常太多了。单元测试不仅可以帮助您缓解这个问题,而且由于其性质,还能够更准确地查明故障位置。