假设我们的课程Controller
取决于课程Service
,Service
课程取决于课程Repository
。只有Repository
与外部系统(比如DB)通信,我知道在执行单元测试时应该嘲笑它。
我的问题:对于单元测试,即使Service
类不直接依赖于任何外部系统,我是否应该在Controller
类被测试时模拟Service
类?为什么?
答案 0 :(得分:2)
这取决于您所编写的测试类型:集成测试或单元测试。 我假设您想在这种情况下编写单元测试。单元测试的目的是单独测试类的业务逻辑,以便模拟每个其他依赖项。
在这种情况下,您将模拟Service
类。这样做还允许您根据传递给Service
的某种方法的输入准备测试某些方案。想象一下,Person findPerson(Long personID)
中有一个Service
- 方法。在测试你的Controller
时,你不想做任何让Service
实际返回正确输出所必需的东西。对于Controller
的某个测试场景,您只希望它返回Person
,而对于不同的测试场景,您不希望它返回任何内容。模拟使这很容易。
另请注意,如果您嘲笑Service
,则不必模仿Repository
,因为您的Service
已经是模拟。
TLDR;在为某个类编写单元测试时,只需模拟每个其他依赖项,以便能够操作对这些依赖项进行的方法调用的输出。
答案 1 :(得分:0)
是的,在测试控制器时模拟服务。单元测试有助于识别回归的位置。因此,如果服务代码已更改,则服务代码测试应该失败,而不是控制器代码已更改。这样,当服务测试失败时,您确定根本原因在于对服务的更改。
此外,通常模拟服务比模拟服务调用的所有存储库只是为了测试控制器要容易得多。因此,它使您的测试更容易维护。
但总的来说,你可能会保留某些类库,因为你通过嘲笑那些比你获得的更多。另见: https://softwareengineering.stackexchange.com/questions/148049/how-to-deal-with-static-utility-classes-when-designing-for-testability
答案 2 :(得分:0)
与所有工程问题一样,TDD也不例外。答案总是,“这取决于”。总是有权衡。
在TDD的情况下,您首先通过行为期望开发测试。根据我的经验,行为期望是一个单元。
一个例子,假设您希望让所有以姓氏“A”开头的用户获得,并且他们在系统中处于活动状态。因此,您可以编写测试来创建控制器操作,以便以“A”public ActionResult GetAllActiveUsersThatStartWithA()
开头的活跃用户。
最后,我可能会有这样的事情:
public ActionResultGetAllActiveUsersThatStartWithA()
{
var users = _repository.GetAllUsers();
var activeUsersThatStartWithA = users.Where(u => u.IsActive && u.Name.StartsWith('A');
return View(activeUsersThatStartWithA);
}
这对我来说是个单位。我现在可以重构(通过使用下面的方法添加service
类来改变我的实现而不改变行为)
public IEnumerable<User> GetActiveUsersThatStartWithLetter(char startWith)
{
var users = _repository.GetAllUsers();
var activeUsersThatStartWithA = users.Where(u => u.IsActive && u.Name.StartsWith(startsWith);
}
我的控制器的新实现变为
public ActionResultGetAllActiveUsersThatStartWithA()
{
return View(_service.GetActiveUsersThatStartWithLetter('A');
}
这显然是一个非常人为的例子,但它给出了我的观点。这样做的主要好处是我的测试不依赖于除repository
之外的任何实现细节。然而,如果我在测试中嘲笑service
,那么我现在与该实现相关联。如果出于任何原因删除了service
图层,我的所有测试都会中断。我发现service
图层更有可能比repository
图层更易变化。
要考虑的另一件事是,如果我在我的控制器类中模拟出service
,我可能遇到一个我的所有测试都正常工作的场景,但我找到系统的唯一方法就是打破是通过集成测试(意味着流程外,或装配组件彼此交互),或通过生产问题。
例如,如果我将service
类的实现更改为:
public IEnumerable<User> GetActiveUsersThatStartWithLetter(char startsWith)
{
throw new Exception();
}
同样,这是一个非常人为的例子,但重点仍然是相关的。我不会用我的controller
测试来理解这一点,因此看起来系统在通过“单元测试”时表现得很好,但实际上系统根本不工作。
我的方法的缺点是测试可能变得非常麻烦。因此,权衡是通过抽象/可模拟实现来平衡测试复杂性。
要记住的关键是TDD带来了回归的好处,但它的主要好处是帮助设计系统。换句话说,不要让设计决定你写的测试。让测试首先决定系统的功能,然后通过重构来担心设计。