如何 Java 对复杂的单元进行单元测试 Class
How To Java Unit Test a Complex Class
我在弄清楚如何解决单元测试问题时遇到了困难。我几乎没有 'unit testing' 经验。我只是在绝对必要的情况下尝试更改 classUnderTest 并进行最少的更改。
我正在使用 JUnit 4, and I am willing to try to use Mockito, JMockit 或任何其他有助于高效且有效地进行单元测试的库。我正在为数据库使用 JDBC。
问题:
在 classUnderTest 中,我正在访问一个 static 数据库。
SpecializedDao specializedDao = SpecializedDao.getSpecializedDao();
ObjectX objectResult = specializedDao.currentObjectXByTimestamp(x, x, x, x, x, x);
并且我正在尝试使用具有有效配置和无效配置的单元测试用例。解决这个问题的正确方法是什么。
我研究过的一些可能解决方案是:
- 不知何故传入,或 'injecting' class ObjectX
的假 ObjectXResult
- 模拟数据库(如果使用静态数据库引用甚至可能,请使用引用假 SpecializedDao 的单独单元测试 servlet class?)
- 使用Mockito with injectMocks, spy, when(methodcallwithparameters) thenReturn result, or some other implementation Mockito提供。
- 接受任何其他解决方案。
问题:
在 classUnderTest 中,我正在使用另一个复杂的处理 class2像这样的工作:
result = class2.execute(x, x, x, x, x, x);
class2 处理内容,returns 结果枚举或一些异常。我如何通过将此特定单元测试的 scope 保持在 classUnderTest 上来处理此问题。 class2 访问数据库,并做了很多工作,这就是为什么它是它自己的 class 但我认为最后三个测试用例取决于彻底测试 classUnderTest.
的处理
感谢您的耐心等待,我尽量把问题说清楚了。
在任何情况下你都需要使用像 Mockito 这样的东西,所以我假设你有那个。
第一个问题:
测试涉及静态调用的代码可能会很痛苦。您可以包含一些字节码操作库,例如 PowerMock
,但我认为这不值得。您可以做的是将静态调用放在包本地方法中,然后使用 spy
将其存根。类似于:
//in class under test:
SpecializedDato getSpecializedDao() {
return SpecializedDao.getSpecializedDao();
}
//in test:
import static org.mockito.Mockito.*;
//...
final SpecializedDao daoMock = mock(SpecializedDao.class);
final ClassUnderTest classUnderTest = spy(new ClassUnderTest());
doReturn(daoMock).when(classUnderTest).getSpecializedDao();
第二题:
如果您发现您的测试变得非常复杂,可能是因为class 被测对象太复杂了。看看您是否可以将功能提取到其他更小的 classes 中。然后,您只需验证是否正在调用这些较小的 classes。
一个好的规则是永远不要从您的 JUnit 连接到外部源。
任何可以执行的操作,例如数据库连接,您都可以使用 Mockito 模拟它们。
使用 Mockito,您可以模拟所有 类 您不想测试的东西。在你的情况下,你可以模拟那些沉重的 类 和 return 一个预期的结果。
在我的办公室,我们通常使用一些解决方案来使复杂的 class 更易于测试。
最不理想的是唯一一个无需修改 class 测试下就可以工作的 - 大量模拟。你嘲笑了你 class 之外的一切。如果您在 class 中有一个静态成员,您可以在开始之前将其设置为带有反射的模拟(可能是您问题的答案)。
这种方法既困难又脆弱,但如果您无法修改正在测试的 class,这几乎是唯一可行的方法。
一个小例外是模拟的一个稍微不同的版本——扩展测试中的 class 并覆盖你的 class 与之交互的所有东西。这对静力学并不总是有效,但如果您的 class 是使用吸气剂设计的,则效果很好。
如果您可以修改 class,我们会使用一些技巧来提供帮助:
1) 始终避免静电。如果你必须有一个单例,通过使用注入框架或自己构建一个迷你注入框架来确保它是可测试的(静态预填充映射应该是一个好的开始)
2) 我们将大量业务逻辑放在纯函数中,我们调用包含所有这些逻辑的 classes "Rules" classes。我们的规则通常可以直接追溯到需求,它们不提供任何自己的数据,一切都传入。它们通常由包含相关数据和其他代码的 OO 外观调用。
3) 降低 classes 的复杂性和交互性。将数据库访问与计算分开。查看每个 class 并确保它只做好一件事。这没有直接帮助,但您会注意到,如果您遵循此规则,编写测试会更简单。
在模拟框架上没有铰链解决方案,处理静态的标准模式只涉及对 CuT 的最小更改是提取静态方法以位于接口后面,而 class 最初访问静态方法现在访问接口上的方法。这个接口的一个实例被注入到 CuT,当不测试时,它是一个小包装器 class,直接委托给原始静态方法。
现在您有了一个可以使用的接口,您可以通过 setter 或构造函数注入来注入该接口的实例,Bob 就是您的叔叔。
这个标准技巧确实涉及一些额外的东西 "stuff" - 一个接口和一个小包装器 class 实现委托给静态的接口,以及一些代码或 DI 框架连接来获取它在代码的生产部分实现 class 以通过构造函数或 setter.
注入 CuT
然后在你的单元测试中,你有一个钩子来注入一个模拟,或者一个手工创建的存根,或者任何你想要的测试替身样式。
在处理你无法控制的第三方库时,这是一种非常常见的模式,它喜欢使用静态。
我最近在 youtube 上发布了一段视频,演示了 Jim Weaver 提到的一些步骤。它是德语的,但可能仍然有帮助。在这个视频中我介绍了一个接口来克服静态调用数据库访问的问题。
https://www.youtube.com/watch?v=KKYro-HGRyk
如果您无法更改所调用方法的源代码,第二个视频演示了一种有用的方法。
如果您关心开发好的、有用的测试,我的建议是不要模拟数据库或其他复杂的classes 您的代码可能与之交互。
相反,请考虑您的测试代码要交付的特定业务场景,并编写一个 现实的(即,不要用便宜的 classes/components 替换真实的 classes/components - 而且通常是不正确的 - 对它们的模仿)和有意义(从现实世界需求的角度来看)测试每个场景。
就是说,如果您仍然想模拟那个 DAO class,可以使用 JMockit 按如下方式完成:
@Test
public void exampleTest(@Mocked SpecializedDao mockDao) {
ObjectX objectResult = new ObjectX();
new Expectations() {{
mockDao.currentObjectXByTimestamp(anyX, anyY, anyZ); result = objectResult;
}};
// Call the SUT.
}
(anyX
等不是实际的 JMockit 参数匹配字段,当然 - 真正的是 anyInt
、anyString
、any
等。 )
我在弄清楚如何解决单元测试问题时遇到了困难。我几乎没有 'unit testing' 经验。我只是在绝对必要的情况下尝试更改 classUnderTest 并进行最少的更改。
我正在使用 JUnit 4, and I am willing to try to use Mockito, JMockit 或任何其他有助于高效且有效地进行单元测试的库。我正在为数据库使用 JDBC。
问题:
在 classUnderTest 中,我正在访问一个 static 数据库。
SpecializedDao specializedDao = SpecializedDao.getSpecializedDao();
ObjectX objectResult = specializedDao.currentObjectXByTimestamp(x, x, x, x, x, x);
并且我正在尝试使用具有有效配置和无效配置的单元测试用例。解决这个问题的正确方法是什么。
我研究过的一些可能解决方案是:
- 不知何故传入,或 'injecting' class ObjectX 的假 ObjectXResult
- 模拟数据库(如果使用静态数据库引用甚至可能,请使用引用假 SpecializedDao 的单独单元测试 servlet class?)
- 使用Mockito with injectMocks, spy, when(methodcallwithparameters) thenReturn result, or some other implementation Mockito提供。
- 接受任何其他解决方案。
问题:
在 classUnderTest 中,我正在使用另一个复杂的处理 class2像这样的工作:
result = class2.execute(x, x, x, x, x, x);
class2 处理内容,returns 结果枚举或一些异常。我如何通过将此特定单元测试的 scope 保持在 classUnderTest 上来处理此问题。 class2 访问数据库,并做了很多工作,这就是为什么它是它自己的 class 但我认为最后三个测试用例取决于彻底测试 classUnderTest.
的处理感谢您的耐心等待,我尽量把问题说清楚了。
在任何情况下你都需要使用像 Mockito 这样的东西,所以我假设你有那个。
第一个问题:
测试涉及静态调用的代码可能会很痛苦。您可以包含一些字节码操作库,例如 PowerMock
,但我认为这不值得。您可以做的是将静态调用放在包本地方法中,然后使用 spy
将其存根。类似于:
//in class under test:
SpecializedDato getSpecializedDao() {
return SpecializedDao.getSpecializedDao();
}
//in test:
import static org.mockito.Mockito.*;
//...
final SpecializedDao daoMock = mock(SpecializedDao.class);
final ClassUnderTest classUnderTest = spy(new ClassUnderTest());
doReturn(daoMock).when(classUnderTest).getSpecializedDao();
第二题:
如果您发现您的测试变得非常复杂,可能是因为class 被测对象太复杂了。看看您是否可以将功能提取到其他更小的 classes 中。然后,您只需验证是否正在调用这些较小的 classes。
一个好的规则是永远不要从您的 JUnit 连接到外部源。 任何可以执行的操作,例如数据库连接,您都可以使用 Mockito 模拟它们。
使用 Mockito,您可以模拟所有 类 您不想测试的东西。在你的情况下,你可以模拟那些沉重的 类 和 return 一个预期的结果。
在我的办公室,我们通常使用一些解决方案来使复杂的 class 更易于测试。
最不理想的是唯一一个无需修改 class 测试下就可以工作的 - 大量模拟。你嘲笑了你 class 之外的一切。如果您在 class 中有一个静态成员,您可以在开始之前将其设置为带有反射的模拟(可能是您问题的答案)。
这种方法既困难又脆弱,但如果您无法修改正在测试的 class,这几乎是唯一可行的方法。
一个小例外是模拟的一个稍微不同的版本——扩展测试中的 class 并覆盖你的 class 与之交互的所有东西。这对静力学并不总是有效,但如果您的 class 是使用吸气剂设计的,则效果很好。
如果您可以修改 class,我们会使用一些技巧来提供帮助:
1) 始终避免静电。如果你必须有一个单例,通过使用注入框架或自己构建一个迷你注入框架来确保它是可测试的(静态预填充映射应该是一个好的开始)
2) 我们将大量业务逻辑放在纯函数中,我们调用包含所有这些逻辑的 classes "Rules" classes。我们的规则通常可以直接追溯到需求,它们不提供任何自己的数据,一切都传入。它们通常由包含相关数据和其他代码的 OO 外观调用。
3) 降低 classes 的复杂性和交互性。将数据库访问与计算分开。查看每个 class 并确保它只做好一件事。这没有直接帮助,但您会注意到,如果您遵循此规则,编写测试会更简单。
在模拟框架上没有铰链解决方案,处理静态的标准模式只涉及对 CuT 的最小更改是提取静态方法以位于接口后面,而 class 最初访问静态方法现在访问接口上的方法。这个接口的一个实例被注入到 CuT,当不测试时,它是一个小包装器 class,直接委托给原始静态方法。
现在您有了一个可以使用的接口,您可以通过 setter 或构造函数注入来注入该接口的实例,Bob 就是您的叔叔。
这个标准技巧确实涉及一些额外的东西 "stuff" - 一个接口和一个小包装器 class 实现委托给静态的接口,以及一些代码或 DI 框架连接来获取它在代码的生产部分实现 class 以通过构造函数或 setter.
注入 CuT然后在你的单元测试中,你有一个钩子来注入一个模拟,或者一个手工创建的存根,或者任何你想要的测试替身样式。
在处理你无法控制的第三方库时,这是一种非常常见的模式,它喜欢使用静态。
我最近在 youtube 上发布了一段视频,演示了 Jim Weaver 提到的一些步骤。它是德语的,但可能仍然有帮助。在这个视频中我介绍了一个接口来克服静态调用数据库访问的问题。
https://www.youtube.com/watch?v=KKYro-HGRyk
如果您无法更改所调用方法的源代码,第二个视频演示了一种有用的方法。
如果您关心开发好的、有用的测试,我的建议是不要模拟数据库或其他复杂的classes 您的代码可能与之交互。
相反,请考虑您的测试代码要交付的特定业务场景,并编写一个 现实的(即,不要用便宜的 classes/components 替换真实的 classes/components - 而且通常是不正确的 - 对它们的模仿)和有意义(从现实世界需求的角度来看)测试每个场景。
就是说,如果您仍然想模拟那个 DAO class,可以使用 JMockit 按如下方式完成:
@Test
public void exampleTest(@Mocked SpecializedDao mockDao) {
ObjectX objectResult = new ObjectX();
new Expectations() {{
mockDao.currentObjectXByTimestamp(anyX, anyY, anyZ); result = objectResult;
}};
// Call the SUT.
}
(anyX
等不是实际的 JMockit 参数匹配字段,当然 - 真正的是 anyInt
、anyString
、any
等。 )