为什么在单元测试中不模拟域对象?

时间:2019-08-28 13:36:01

标签: java unit-testing mocking

我给您2个测试;的目的是唯一地以确认在调用service.doSomething时,以该人的电子邮件作为参数调用了emailService.sendEmail

@Mock 
private EmailService emailService;

@InjectMocks
private Service service;

@Captor
private ArgumentCaptor<String> stringCaptor;

@Test
public void test_that_when_doSomething_is_called_sendEmail_is_called_NO_MOCKING() {

    final String email = "billy.tyne@myspace.com";

    // There is only one way of building an Address and it requires all these fields
    final Address crowsNest = new Address("334", "Main Street", "Gloucester", "MA", "01930", "USA");
    // There is only one way of building a Phone and it requires all these fields
    final Phone phone = new Phone("1", "978-281-2965");
    // There is only one way of building a Vessel and it requires all these fields
    final Vessel andreaGail = new Vessel("Andrea Gail", "Fishing", 92000);
    // There is only one way of building a Person and it requires all these fields
    final Person captain = new Person("Billy", "Tyne", email, crowsNest, phone, andreaGail);

    service.doSomething(captain); // <-- This requires only the person's email to be initialised, it doesn't care about anything else

    verify(emailService, times(1)).sendEmail(stringCaptor.capture());

    assertThat(stringCaptor.getValue(), eq(email));   
}

@Test
public void test_that_when_doSomething_is_called_sendEmail_is_called_WITH_MOCKING() {

    final String email = "billy.tyne@myspace.com";

    final Person captain = mock(Person.class);
    when(captain.getEmail()).thenReturn(email);

    service.doSomething(captain); // <-- This requires the person's email to be initialised, it doesn't care about anything else

    verify(emailService, times(1)).sendEmail(stringCaptor.capture());   

    assertThat(stringCaptor.getValue(), eq(email));   
}

为什么我的团队告诉我不要模拟运行测试所需的域对象,而不是模拟实际测试的一部分?我被告知模拟仅用于测试服务的依赖项。我认为,生成的测试代码更加精简,清晰和易于理解。测试的目的是验证对emailService.sendEmail的调用是否发生,这没有任何干扰。这是我长期以来在许多工作上所听到并接受的福音。但是我仍然不同意。

4 个答案:

答案 0 :(得分:4)

我想我了解您的团队的位置。

他们可能说您应该为那些难以实例化的依赖项保留模拟。这包括对数据库进行调用的存储库,以及可能具有自己的依赖关系的其他服务。它不包括可以实例化的域对象(即使填写所有构造函数参数也很麻烦)。

如果模拟域对象,则测试不会为您提供任何代码覆盖率。我知道我宁愿让这些域对象尽可能多地包含在服务,控制器,存储库等的测试中,并尽量减少为直接行使其getter和setter方法而编写的测试。这样一来,域对象的测试就可以专注于任何实际的业务逻辑。

这确实意味着,如果域对象有错误,则对多个组件的测试可能会失败。我认为可以。我仍然要对域对象进行测试(因为相对于确保所有路径都覆盖在服务的测试中,进行隔离测试要容易得多),但是我不想完全依赖域对象测试来准确地进行测试。反映这些对象在服务中的使用方式,似乎要问的太多了。

您有一点要说,模拟允许您在不填充所有数据的情况下制作对象(而且我敢肯定,实际代码会比所发布的代码差 lot )。这是一个折衷,但是对代码覆盖范围包括实际的域对象以及被测试的服务,对我来说似乎是一个更大的胜利。

在我看来,您的团队选择在实用主义与纯正方面犯错。如果其他所有人都达成了共识,则需要尊重这一共识。有些事情值得一挥。这不是其中之一。

答案 1 :(得分:1)

这里有两个错误。

首先,测试是否在调用服务方法时将其委托给另一个方法。这是一个糟糕的规范。应该根据服务方法返回的值(对于getter)或随后可以通过该服务接口获取的值(对于mutator)来指定服务方法。服务层应视为外观。通常,应根据很少的方法指定它们委托给哪些方法以及何时委托。委托是实现的详细信息,因此不应进行测试。

不幸的是,流行的模拟框架鼓励这种错误的方法。热心使用行为驱动开发也是如此。

第二个错误集中在 unit 测试的概念上。我们希望我们的每个单元测试都可以测试一件事,因此,如果一件事情中存在故障,那么我们将有一个测试失败,并且很容易找到故障。我们倾向于认为“单元”的含义与“方法”或“类”相同。这使人们认为单元测试应该只涉及一个真实的类,而所有其他类都应该被模拟。除了最简单的类,这对所有其他人都是不可能的。几乎所有Java代码都使用标准库中的类,例如StringHashSet。大多数专业的Java代码都使用来自各种框架的类,例如Spring。没有人认真建议嘲笑那些人。我们接受那些类是可信赖的,因此不需要嘲笑。我们接受不模拟我们单位代码使用的“可信任”类是可以的。但是,您说的是,我们的类是不可信的,因此我们必须模拟它们。不是这样通过对它们进行良好的单元测试,您可以 信任其他类。但是,当仅存在一个故障时,如何避免相互依存的类纠缠而导致大量的测试失败呢?那将是调试的噩梦!使用1970年代编程中的一个概念(称为虚拟机层次结构,鉴于虚拟机的其他含义,现在这是一个相当混乱的术语):安排您的软件从低层到高层,高层由较低层执行操作。每一层都提供了一种更具表现力或高级的抽象描述操作和对象的方式。因此,域对象处于较低级别,服务层处于较高级别。当多个测试失败时,开始调试最低级别的测试失败:故障可能在该层中,可能(但可能不是)在较低的层中,而不是在较高的层中。

仅对将使测试运行非常昂贵的输入和输出接口保留模拟(通常,这意味着模拟存储库层和日志记录接口)。

答案 2 :(得分:0)

这是一个折衷,您已经很好地将示例设计为“处于边缘”。通常,应该出于某种原因进行模拟。充分的理由是:

  • 您不能轻易地使组件依赖(DOC)的行为符合测试的预期。
  • 调用DOC是否会引起任何非专业行为(日期/时间,随机性,网络连接)?
  • 测试设置过于复杂和/或维护密集(例如,需要外部文件)(*见下文)
  • 原始DOC为您的测试代码带来了可移植性问题。
  • 使用原始DOC是否会导致构建/执行时间过长?
  • 是否存在使测试不可靠的DOC稳定性(成熟度)问题,或者更糟糕的是,甚至还没有DOC?

例如,您(通常)不会模拟标准库数学函数,例如sincos,因为它们没有上述任何问题。

为什么建议避免在不必要的地方进行嘲笑?

  • 一方面,模拟会增加测试的复杂性。
  • 第二,模拟使测试取决于代码的内部工作原理,即代码与DOC的交互方式(例如,在您的情况下,使用getFirstName获得船长的名字,尽管可能存在另一种获取信息的方法。
  • 而且,正如Nathan所提到的那样,它可以看作是一个加号-在不进行模拟的情况下-DOC是免费测试的-尽管我在这里要小心:如果您倾向于尝试进行测试,则可能会失去测试的重点DOC。 DOC应该有自己的测试。

为什么您的方案“处于边缘”?

上面提到的进行模拟的良好原因之一用(*)标记:“测试设置过于复杂...”,而您的示例被构造为具有有点复杂的测试设置。测试设置的复杂性显然不是一个硬标准,开发人员只需选择一个。如果您希望以这种方式查看它,则可以说这两种方法在将来的维护方案中都存在一些风险。

总而言之,我要说的是,任何一个立场(通常是嘲笑或通常不嘲笑)都不对。相反,开发人员应了解决策标准,然后将其应用于特定情况。而且,当方案处于灰色区域,以致标准不能导致明确的决定时,请不要为之争辩。

答案 3 :(得分:-1)

自动测试的目的是揭示某个软件单元的预期行为不再按预期执行(又称“发现错误”)

给定测试套件中被测试单元的粒度/大小/范围由您和您的团队决定。

一旦确定,如果可以在不牺牲测试行为的情况下模拟超出该范围的某些东西,则意味着它显然与测试无关,应该 em>被嘲笑。这将有助于使您的测试更多:

  • 已隔离
  • 快速
  • 可读(如您所述)

...而且最重要的是,当测试失败时,将表明某个软件单元的预期行为不再按预期执行。给定足够小的受测单元,很明显该错误发生在哪里以及为什么。

如果无法进行模拟测试的示例失败,则可能表明AddressPhoneVesselPerson有问题。这将导致浪费时间来精确跟踪错误发生的位置。

我要提到的一件事是,您的模拟示例实际上是有点难以理解的IMO,因为您断言String的值为"Billy",但不清楚原因。