如何在隔离 class 待测对象的同时测试对象是否正确创建?

How to test if the object is created correctly while keeping the class under test in isolation?

人们常说编写单元测试必须只测试一个 class 并模拟所有协作者。我正在尝试学习 TDD 以使我的代码设计更好,但现在我陷入了应该打破此规则的境地。还是不应该?

一个例子:测试中的class有一个方法获取Person,根据Person和returns创建Employee Employee.

public class EmployeeManager {

    private DataMiner dataMiner;

    public Employee getCoolestEmployee() {
        Person dankestPerson = dataMiner.getDankestPerson();
        Employee employee = new Employee();
        employee.setName(dankestPerson.getName() + "bug in my code");
        return employee;
    }

    // ...
}

Employee 应该被视为合作者吗?如果不是,为什么不呢?如果是,我该如何正确测试 'Employee' 是否已正确创建?

这是我想到的测试(使用 JUnit 和 Mockito):

@Test
public void coolestEmployeeShouldHaveDankestPersonsName() {
    when(dataMinerMock.getDankestPerson()).thenReturn(dankPersonMock);
    when(dankPersonMock.getName()).thenReturn("John Doe");

    Employee coolestEmployee = employeeManager.getCoolestEmployee();
    assertEquals("John Doe", coolestEmployee.getName());
}

如您所见,我必须使用 coolestEmployee.getName() - Employee class 的方法,该方法未在测试中。

想到的一个可能的解决方案是将 Person 转换为 Employee 的任务提取到 Employee class 的新方法中,类似

public Employee createFromPerson(Person person);

我是不是想多了?正确的做法是什么?

单元测试的目标是快速可靠地确定单个系统是否损坏。这并不意味着你需要模拟周围的整个世界,只是你应该确保你使用的协作者是快速的、确定性的并且经过良好测试的。

数据对象——尤其是 POJO 和生成的值对象——趋于稳定且经过良好测试,依赖性极低。像其他重状态对象一样,模拟它们也往往非常乏味,因为模拟框架往往对状态没有强大的控制(例如 getX 应该 return nsetX(n)).假设 Employee 是一个数据对象,它很可能是单元测试中实际使用的一个很好的候选者,前提是它包含的任何逻辑都经过了良好的测试。

一般不要嘲笑的其他合作者:

  • JRE classes 和接口。 (例如,永远不要模拟 List。它无法阅读,你的测试也不会因此变得更好。)
  • 确定性第三方 classes。 (如果任何 classes 或方法更改为 final,您的模拟将失败;此外,如果您使用的是库的稳定版本,它也不是虚假的失败源。)
  • 有状态 classes,只是因为模拟在测试交互方面比状态要好得多。考虑一个假的,或者其他一些测试替身。
  • 快速且经过良好测试的其他 class 几乎没有依赖项。如果您对系统有信心,并且对测试的确定性或速度没有危害,则无需模拟它。

那还剩下什么? 不确定的或缓慢的服务 class你写的 es 或 wrapper,它们是无状态的或在你的开发过程中变化很小测试,并且可能有许多自己的合作者。在这些情况下,很难使用实际的 class 编写快速且确定性的测试,因此使用测试替身很有意义——使用模拟创建测试替身非常容易框架。

另请参阅: Martin Fowler 的文章 "Mocks Aren't Stubs",其中讨论了各种测试替身及其优缺点。

只读取私有字段的获取器通常不值得测试。默认情况下,您可以在其他测试中非常安全地依赖它们。因此,我不会担心在 EmployeeManager 的测试中使用 dankestPerson.getName()

就测试而言,您的测试没有任何问题。生产代码的设计可能不同——模拟 dankestPerson 可能意味着它有一个接口或抽象基础 class,这可能是过度工程的标志,尤其是对于业务实体而言。我要做的只是新建一个 Person,将其名称设置为预期值并将 dataMinerMock 设置为 return。

此外,在 class 名称中使用 "Manager" 可能表明缺乏凝聚力和职责范围太广。