只有当给定的代码块对于集成测试来说太复杂时,才应该对它使用单元测试吗?
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 创建一个。
对于仅使用集成测试的参数,
如果考虑到应用程序的实际使用情况,用户将只使用控制器,集成测试将涵盖最终用户的场景。
如果在中间层进行了更改(例如,如果通过将存储库方法 FindForUser(int userId)
更改为 FindForUser(int userId,List<Status> statuses))
来仅获取具有特定状态的订单,则将是最小的需要在集成测试中进行更改(在接受状态之外添加额外的订单),其中大部分测试需要在单元测试中更改(2 个测试需要更改,需要添加更多测试。
对于仅使用单元测试的参数,
你可以更快地指出失败——然而,大多数时候失败是由于方法签名的改变(因为有人忘记配置模拟对象)
更好的文档——好的集成测试文档应该足以识别客户需求,在我看来这已经足够好了。
考虑到以上所有内容,我不确定为什么我们实际上需要单元测试,而我们实际上可以通过使用集成测试获得更好的结果。 (当然有人可能会说集成测试很昂贵,但我会说它并不昂贵,因为当业务需求发生变化时,开发人员花在修改单元测试上的时间)
你的例子太简单了,不具有代表性。该代码中没有复杂的逻辑。当你有很多 ifs 和循环时,它就会改变。
假设您的应用程序中有 n if
个语句。这意味着您有 2^n 个可能的不同执行流程。所以你可以用 2 个单元测试来测试每个 if
,因此你有 2n 个单元测试。或者您只能进行集成测试。但是你需要 2^n 次测试。
循环使问题更加复杂。因此,当您没有逻辑 (getters/setters/sequences) 时,您可能根本不需要任何测试。但是当你有复杂的逻辑时,也许最好单独测试那个复杂的东西
另一件事是集成测试的难度。例如,您想测试仅在年底运行的代码。在单元测试中准备环境(如时间)比在集成测试中更容易,在集成测试中你不应该过多地干扰系统的内部
单元测试和集成测试有不同的用途:
- 通过单元测试,您可以验证给定组件 是否按预期工作
- 通过集成测试,您可以验证一组组件是否 协同工作,而无需验证特定组件的详细信息
您的案例非常微不足道,单元测试可能无法为 UserOrderController
提供超过一个体面的集成测试。但这只是因为你的类非常小,不包含复杂的逻辑。
但是,当 UserOrderWorker
变得更复杂时会发生什么?
- 如果
_userRepo
returns null 因为没有找到用户怎么办?
- 如果
_userRepo
因连接数据库超时而抛出异常怎么办?
- 如果引入新要求说对于特定用户 (parents) 您还需要提取其 children 订单怎么办?
要使用集成测试来测试此类场景,您必须对每个特定案例进行单独测试。乘以参与集成测试的组件数。这通常太多了。单元测试不仅可以帮助您缓解这个问题,而且由于其性质,还能够更准确地查明故障位置。
我们很久以来就有这个问题,而且经常不时地浮出水面,争论不休。在我们开发的系统中,我们习惯于在不同层(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 创建一个。
对于仅使用集成测试的参数,
如果考虑到应用程序的实际使用情况,用户将只使用控制器,集成测试将涵盖最终用户的场景。
如果在中间层进行了更改(例如,如果通过将存储库方法
FindForUser(int userId)
更改为FindForUser(int userId,List<Status> statuses))
来仅获取具有特定状态的订单,则将是最小的需要在集成测试中进行更改(在接受状态之外添加额外的订单),其中大部分测试需要在单元测试中更改(2 个测试需要更改,需要添加更多测试。
对于仅使用单元测试的参数,
你可以更快地指出失败——然而,大多数时候失败是由于方法签名的改变(因为有人忘记配置模拟对象)
更好的文档——好的集成测试文档应该足以识别客户需求,在我看来这已经足够好了。
考虑到以上所有内容,我不确定为什么我们实际上需要单元测试,而我们实际上可以通过使用集成测试获得更好的结果。 (当然有人可能会说集成测试很昂贵,但我会说它并不昂贵,因为当业务需求发生变化时,开发人员花在修改单元测试上的时间)
你的例子太简单了,不具有代表性。该代码中没有复杂的逻辑。当你有很多 ifs 和循环时,它就会改变。
假设您的应用程序中有 n if
个语句。这意味着您有 2^n 个可能的不同执行流程。所以你可以用 2 个单元测试来测试每个 if
,因此你有 2n 个单元测试。或者您只能进行集成测试。但是你需要 2^n 次测试。
循环使问题更加复杂。因此,当您没有逻辑 (getters/setters/sequences) 时,您可能根本不需要任何测试。但是当你有复杂的逻辑时,也许最好单独测试那个复杂的东西
另一件事是集成测试的难度。例如,您想测试仅在年底运行的代码。在单元测试中准备环境(如时间)比在集成测试中更容易,在集成测试中你不应该过多地干扰系统的内部
单元测试和集成测试有不同的用途:
- 通过单元测试,您可以验证给定组件 是否按预期工作
- 通过集成测试,您可以验证一组组件是否 协同工作,而无需验证特定组件的详细信息
您的案例非常微不足道,单元测试可能无法为 UserOrderController
提供超过一个体面的集成测试。但这只是因为你的类非常小,不包含复杂的逻辑。
但是,当 UserOrderWorker
变得更复杂时会发生什么?
- 如果
_userRepo
returns null 因为没有找到用户怎么办? - 如果
_userRepo
因连接数据库超时而抛出异常怎么办? - 如果引入新要求说对于特定用户 (parents) 您还需要提取其 children 订单怎么办?
要使用集成测试来测试此类场景,您必须对每个特定案例进行单独测试。乘以参与集成测试的组件数。这通常太多了。单元测试不仅可以帮助您缓解这个问题,而且由于其性质,还能够更准确地查明故障位置。