如何在隔离 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 n
在 setX(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" 可能表明缺乏凝聚力和职责范围太广。
人们常说编写单元测试必须只测试一个 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 n
在 setX(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" 可能表明缺乏凝聚力和职责范围太广。