提交 e979de7c 编写于 作者: W wizardforcel

2021-10-01 21:15:47

上级 8bf599e3
......@@ -36,7 +36,7 @@
第 13 章“使用新的日期和时间 API*”演示了如何构造时区相关和独立的日期和时间实例,如何在日期实例之间创建基于日期和时间的时段,如何表示历元时间,如何操作和比较日期和时间实例,如何使用不同的日历系统,以及如何使用`DateTimeFormatter`设置日期格式。
第 14 章“测试”解释了如何在 API 与其他组件集成之前对其进行单元测试,包括使用一些虚拟数据的存根依赖和模拟依赖。我们还将向您展示如何编写 fixture 来填充测试数据,以及如何通过集成不同的 api 并测试它们来测试您的应用程序行为。
第 14 章“测试”解释了如何在 API 与其他组件集成之前对其进行单元测试,包括使用一些虚拟数据的依赖和模拟依赖。我们还将向您展示如何编写 fixture 来填充测试数据,以及如何通过集成不同的 api 并测试它们来测试您的应用程序行为。
第 15 章“使用 Java 10 和 Java 11 进行编码的新方法”,演示了如何使用局部变量类型推断,以及何时和如何使用局部变量语法进行 lambda 参数。
......
# 测试
本章介绍如何测试应用程序如何捕获和自动测试用例,如何在 API 与其他组件集成之前对其进行单元测试,以及如何集成所有单元。我们将向您介绍**行为驱动开发****BDD**),并展示它如何成为您应用程序开发的起点。我们还将演示 JUnit 框架如何用于单元测试。有时,在单元测试期间,我们必须使用一些虚拟数据来存根依赖项,这可以通过模拟依赖项来完成。我们将向您展示如何使用模拟库来实现这一点。我们还将向您展示如何编写 fixture 来填充测试数据,以及如何通过集成不同的 api 并将它们测试在一起来测试应用程序的行为。我们将介绍以下配方:
本章介绍如何测试应用程序如何捕获和自动测试用例,如何在 API 与其他组件集成之前对其进行单元测试,以及如何集成所有单元。我们将向您介绍**行为驱动开发****BDD**),并展示它如何成为您应用程序开发的起点。我们还将演示 JUnit 框架如何用于单元测试。有时,在单元测试期间,我们必须使用一些虚拟数据来插桩依赖项,这可以通过模拟依赖项来完成。我们将向您展示如何使用模拟库来实现这一点。我们还将向您展示如何编写 fixture 来填充测试数据,以及如何通过集成不同的 api 并将它们测试在一起来测试应用程序的行为。我们将介绍以下配方:
* 黄瓜行为测试
* 使用 JUnit 对 API 进行单元测试
......
此差异已折叠。
......@@ -38,14 +38,14 @@
换句话说,我们想要测试被测的**系统****SUT**),而不是它的**依赖于组件****文档**)。为了实现这种隔离,我们通常使用*测试加倍*来替换这些文档。模拟对象是一种双重测试,它是按照对真实文档的期望进行编程的。
简单地说,Mockito 是一个测试框架,允许创建、存根和验证模拟对象。为此,Mockito 提供了一个 API 来隔离 SUT 及其文档。一般来说,使用 Mockito 包括三个不同的步骤:
简单地说,Mockito 是一个测试框架,允许创建、插桩和验证模拟对象。为此,Mockito 提供了一个 API 来隔离 SUT 及其文档。一般来说,使用 Mockito 包括三个不同的步骤:
1. **Mocking objects**:为了隔离我们的 SUT,我们使用 Mockito API 创建其关联文档的 mock。这样,我们保证 SUT 不依赖于它的实际文档,而我们的单元测试实际上关注于 SUT。
2. **设置期望值**:mock 对象与其他测试双重对象(如存根)的区别在于,可以根据单元测试的需要,使用自定义期望值对 mock 对象进行编程。Mockito 术语中的这个过程称为 stubing 方法,其中这些方法属于 mock。默认情况下,模拟对象模仿真实对象的行为。实际上,这意味着模拟对象返回适当的伪值,例如布尔类型为 false,对象为 null,整数或长返回类型为 0,等等。Mockito 允许我们使用一个丰富的 API 来改变这种行为,它允许存根在调用方法时返回一个特定的值。
2. **设置期望值**:mock 对象与其他测试替身对象(如桩)的区别在于,可以根据单元测试的需要,使用自定义期望值对 mock 对象进行编程。Mockito 术语中的这个过程称为 stubing 方法,其中这些方法属于 mock。默认情况下,模拟对象模仿真实对象的行为。实际上,这意味着模拟对象返回适当的伪值,例如布尔类型为 false,对象为 null,整数或长返回类型为 0,等等。Mockito 允许我们使用一个丰富的 API 来改变这种行为,它允许桩在调用方法时返回一个特定的值。
当一个模拟对象没有任何预期(即没有*存根方法*时,从技术上讲,它不是*模拟*对象,而是*虚拟*对象(请看第一章、*软件质量和 Java 测试回顾*对于定义)。
当一个模拟对象没有任何预期(即没有*方法*时,从技术上讲,它不是*模拟*对象,而是*虚拟*对象(请看第一章、*软件质量和 Java 测试回顾*对于定义)。
3. **验证**:最后,我们正在创建测试,因此,我们需要对 SUT 进行某种验证。Mockito 提供了一个强大的 API 来执行不同类型的验证。使用此 API,我们评估与 SUT 和文档的交互,使用模拟验证调用顺序,或者捕获并验证传递给存根方法的参数。此外,可以使用 JUnit 的内置断言功能或使用第三方断言库(例如,Hamcrest、AssertJ 或 Truth)来补充 Mockito 的验证功能。参见第 3 章*JUnit 5 标准测试*中的*断言*部分。
3. **验证**:最后,我们正在创建测试,因此,我们需要对 SUT 进行某种验证。Mockito 提供了一个强大的 API 来执行不同类型的验证。使用此 API,我们评估与 SUT 和文档的交互,使用模拟验证调用顺序,或者捕获并验证传递给方法的参数。此外,可以使用 JUnit 的内置断言功能或使用第三方断言库(例如,Hamcrest、AssertJ 或 Truth)来补充 Mockito 的验证功能。参见第 3 章*JUnit 5 标准测试*中的*断言*部分。
下表总结了按上述阶段分组的 Mockito API:
......@@ -53,23 +53,23 @@
| --- | --- | --- |
| `@Mock` | 此注释标识由 Mockito 创建的模拟对象。这通常用于单据。 | 1.模拟对象 |
| `@InjectMocks` | 此注释标识将在其中注入模拟的对象。这通常用于我们要测试的单元,即我们的 SUT。 | 1.模拟对象 |
| `@Spy` | 除了 mock 之外,Mockito 还允许我们创建 spy 对象(即部分 mock 实现,因为它们在非存根方法中使用真实实现)。 | 1.模拟对象 |
| `Mockito.when(x).thenReturn(y)``Mockito.doReturn(y).when(x)` | 这些方法允许我们指定给定模拟对象的存根方法(`x`应该返回的值(`y`。 | 2.设定期望值(*存根方式*) |
| `Mockito.when(x).thenThrow(e)``Mockito.doThrow(e).when(x)` | 这些方法允许我们指定调用给定模拟对象的存根方法(`x`时应引发的异常(`e`)。 | 2.设定期望值(*存根方式*) |
| `Mockito.when(x).thenAnswer(a)``Mockito.doAnswer(a).when(x)` | 与返回硬编码值不同,当调用模拟的给定方法(`x`时,将执行动态用户定义逻辑(`Answer a`)。 | 2.设定期望值(*存根方式*) |
| `Mockito.when(x).thenCallRealMethod()``Mockito.doCallRealMethod().when(x)` | 这个方法允许我们真正实现一个方法,而不是模拟的方法。 | 2.设定期望值(*存根方式*) |
| `Mockito.doNothing().when(x)` | 使用间谍时,默认行为是调用对象的实际方法。为了避免执行`void`方法`x`,使用此方法。 | 2.设定期望值(*存根方式*) |
| `BDDMockito.given(x).willReturn(y)``BDDMockito.given(x).willThrow(e)``BDDMockito.given(x).willAnswer(a)``BDDMockito.given(x).willCallRealMethod()` | 行为驱动开发是一种测试方法,在这种方法中,测试是根据场景指定的,并在给定的(初始上下文)、(事件发生时的*和*然后*(确保某些结果)时实施。Mockito 通过类`BDDMockito`支持这种类型的测试。存根方法(`x`的行为等同于`Mockito.when(x)`。* | 2.设定期望值(*存根方式*) |
| `@Spy` | 除了 mock 之外,Mockito 还允许我们创建 spy 对象(即部分 mock 实现,因为它们在非方法中使用真实实现)。 | 1.模拟对象 |
| `Mockito.when(x).thenReturn(y)``Mockito.doReturn(y).when(x)` | 这些方法允许我们指定给定模拟对象的桩方法(`x`应该返回的值(`y`。 | 2.设定期望值(*插桩方式*) |
| `Mockito.when(x).thenThrow(e)``Mockito.doThrow(e).when(x)` | 这些方法允许我们指定调用给定模拟对象的桩方法(`x`时应引发的异常(`e`)。 | 2.设定期望值(*插桩方式*) |
| `Mockito.when(x).thenAnswer(a)``Mockito.doAnswer(a).when(x)` | 与返回硬编码值不同,当调用模拟的给定方法(`x`时,将执行动态用户定义逻辑(`Answer a`)。 | 2.设定期望值(*插桩方式*) |
| `Mockito.when(x).thenCallRealMethod()``Mockito.doCallRealMethod().when(x)` | 这个方法允许我们真正实现一个方法,而不是模拟的方法。 | 2.设定期望值(*插桩方式*) |
| `Mockito.doNothing().when(x)` | 使用间谍时,默认行为是调用对象的实际方法。为了避免执行`void`方法`x`,使用此方法。 | 2.设定期望值(*插桩方式*) |
| `BDDMockito.given(x).willReturn(y)``BDDMockito.given(x).willThrow(e)``BDDMockito.given(x).willAnswer(a)``BDDMockito.given(x).willCallRealMethod()` | 行为驱动开发是一种测试方法,在这种方法中,测试是根据场景指定的,并在给定的(初始上下文)、(事件发生时的*和*然后*(确保某些结果)时实施。Mockito 通过类`BDDMockito`支持这种类型的测试。桩方法(`x`的行为等同于`Mockito.when(x)`。* | 2.设定期望值(*插桩方式*) |
| `Mockito.verify()` | 此方法验证模拟对象的调用。可以选择使用以下方法增强此验证: | 3.核查 |
| | `times(n)`:stubbed 方法被精确调用`n`次。 | |
| | `never()`:从未调用存根方法。 | |
| | `never()`:从未调用方法。 | |
| | `atLeastOnce()`:stubbed 方法至少调用一次。 | |
| | `atLeast(n)`:stubbed 方法至少被调用 n 次。 | |
| | `atMost(n)`:stubbed 方法最多调用 n 次。 | |
| | `only()`:如果对 mock 对象调用任何其他方法,mock 将失败。 | |
| | `timeout(m)`:此方法最多在`m`毫秒内调用。 | |
| `Mockito.verifyZeroInteractions()``Mockito.verifyNoMoreInteractions()` | 这两个方法验证存根方法没有交互。在内部,它们使用相同的实现。 | 3.核查 |
| `@Captor` | 这个注释允许我们定义一个`ArgumentChaptor`对象,目的是验证传递给存根方法的参数。 | 3.核查 |
| `Mockito.verifyZeroInteractions()``Mockito.verifyNoMoreInteractions()` | 这两个方法验证方法没有交互。在内部,它们使用相同的实现。 | 3.核查 |
| `@Captor` | 这个注释允许我们定义一个`ArgumentChaptor`对象,目的是验证传递给方法的参数。 | 3.核查 |
| `Mockito.inOrder` | 它有助于验证与模拟的交互是否按给定顺序执行。 | 3.核查 |
使用上表中描述的不同注释(`@Mock``@InjectMocks``@Spy``@Captor`)是可选的,尽管对于测试可读性的影响是推荐的。换句话说,除了使用不同的 Mockito 类使用注释外,还有其他选择。例如,为了创建一个`Mock`,我们可以使用注释`@Mock`,如下所示:
......@@ -338,7 +338,7 @@ public class LoginRepository {
现在,我们将使用 JUnit5 和 Mockito 测试我们的系统。首先,我们测试控制器组件。因为我们正在进行单元测试,所以需要将`LoginController`登录与系统的其余部分隔离开来。要做到这一点,我们需要模拟它的依赖关系,在本例中是`LoginService`组件。使用前面解释的 SUT/DOC 术语,在这个测试中,我们的 SUT 是类`LoginController`,它的 DOC 是类`LoginService`
为了用 JUnit5 实现我们的测试,首先我们需要用`@ExtendWith`注册`MockitoExtension`。然后,我们用`@InjectMocks`(类别`LoginController`)声明 SUT,用`@Mock`(类别`LoginService`声明其 DOC。我们实施了两个测试(`@Test`。第一个(`testLoginOk`指定调用 mock`loginService`的方法 login 时,该方法应返回 true。之后,实际执行 SUT,并验证其响应(在这种情况下,返回的字符串必须是`OK`。此外,Mockito API 再次用于评估是否不再与 mock`LoginService`进行交互。第二个测试(`testLoginKo`)是等效的,但是将方法 login 存根为返回 false,因此在这种情况下 SUT`(LoginController)`的响应必须是`KO`
为了用 JUnit5 实现我们的测试,首先我们需要用`@ExtendWith`注册`MockitoExtension`。然后,我们用`@InjectMocks`(类别`LoginController`)声明 SUT,用`@Mock`(类别`LoginService`声明其 DOC。我们实施了两个测试(`@Test`。第一个(`testLoginOk`指定调用 mock`loginService`的方法 login 时,该方法应返回 true。之后,实际执行 SUT,并验证其响应(在这种情况下,返回的字符串必须是`OK`。此外,Mockito API 再次用于评估是否不再与 mock`LoginService`进行交互。第二个测试(`testLoginKo`)是等效的,但是将方法 login 插桩为返回 false,因此在这种情况下 SUT`(LoginController)`的响应必须是`KO`
```java
package io.github.bonigarcia;
......@@ -632,7 +632,7 @@ class LoginServiceChaptorTest {
使用 JUnit 5 和 Mockito 执行*LoginServiceChaptorTest*的单元测试
我们在本章中看到的最后一个与 Mockito 有关的例子与间谍的使用有关。正如前面介绍的,默认情况下,间谍在非存根方法中使用实际实现。因此,如果我们不在 spy 对象中存根方法,我们得到的就是测试中的真实对象。这就是下一个示例中发生的情况。如我们所见,我们使用`LoginService`作为我们的 SUT,然后我们监视对象`LoginRepository`。由于在测试主体中,我们没有在 spy 对象中编程期望,因此我们正在测试中评估真实系统。
我们在本章中看到的最后一个与 Mockito 有关的例子与间谍的使用有关。正如前面介绍的,默认情况下,间谍在非桩方法中使用实际实现。因此,如果我们不在 spy 对象中桩方法,我们得到的就是测试中的真实对象。这就是下一个示例中发生的情况。如我们所见,我们使用`LoginService`作为我们的 SUT,然后我们监视对象`LoginRepository`。由于在测试主体中,我们没有在 spy 对象中编程期望,因此我们正在测试中评估真实系统。
总之,测试数据准备好获得正确的登录(使用用户名为`user`,密码为`p1`,这在`LoginRepository`的实际实现中存在于硬编码值中),然后获得一些不成功登录的伪值:
......@@ -1258,7 +1258,7 @@ class IndexTest {
容器内首次测试的控制台输出
第二个测试类似,但作为一个差异因素,它使用测试能力`@MockBean`通过模拟覆盖Spring组件(在本例中为`PageService`)。在测试主体中,首先我们将模拟的方法`getPage`存根,以将组件的默认响应更改为`redirect:/page.html`。因此,当使用对象`MockMvc`在测试中请求资源`/`时,我们将获得一个 HTTP 302 响应(重定向)到资源`/page.html`(实际上是一个现有页面,如项目截图所示):
第二个测试类似,但作为一个差异因素,它使用测试能力`@MockBean`通过模拟覆盖Spring组件(在本例中为`PageService`)。在测试主体中,首先我们将模拟的方法`getPage`插桩,以将组件的默认响应更改为`redirect:/page.html`。因此,当使用对象`MockMvc`在测试中请求资源`/`时,我们将获得一个 HTTP 302 响应(重定向)到资源`/page.html`(实际上是一个现有页面,如项目截图所示):
```java
package io.github.bonigarcia;
......@@ -2150,7 +2150,7 @@ class SpringBootRestTest {
使用 TestRestTemplate 验证 REST 服务的 Jupiter 测试的输出。
在本节结束时,我们将看到一个示例,其中使用 [WireMock](http://wiremock.org/) 库。该库允许模拟 REST 服务,即所谓的 HTTP*模拟服务器*。此模拟服务器捕获对服务的传入请求,并提供存根响应。此功能对于测试使用 REST 服务的系统非常有用,但该服务在测试期间不可用(或者我们可以测试单独调用该服务的组件)。
在本节结束时,我们将看到一个示例,其中使用 [WireMock](http://wiremock.org/) 库。该库允许模拟 REST 服务,即所谓的 HTTP*模拟服务器*。此模拟服务器捕获对服务的传入请求,并提供响应。此功能对于测试使用 REST 服务的系统非常有用,但该服务在测试期间不可用(或者我们可以测试单独调用该服务的组件)。
像往常一样,我们看到一个示例来演示它的用法。假设我们有一个使用远程 REST 服务的系统。为了实现该服务的客户机,我们使用 [Retrofit2](http://square.github.io/retrofit/),这是一个高度可配置的 Java HTTP 客户端。我们定义了使用此服务的接口,如下面的类所示。请注意,该服务公开了三个用于读取远程文件的端点(打开文件、读取流和关闭流):
......@@ -2218,7 +2218,7 @@ import retrofit2.converter.gson.GsonConverterFactory;
}
```
最后,我们实现了一个 JUnit5 测试来验证我们的服务。注意,我们正在创建模拟服务器(`**new** WireMockServer`,并使用 WireMock 在测试设置(`@BeforeEach`中提供的静态方法`stubFor(...)`对 REST 服务调用进行存根。由于在本例中,SUT 非常简单,并且没有文档,因此我们也在每个测试的设置中直接实例化了类`RemoteFileService`,使用模拟服务器 URL 作为构造函数参数。最后,我们测试我们的服务(使用模拟服务器),在本例中,通过调用方法`getFile`并评估其输出,简单地运行名为`wireMockServer`的对象。
最后,我们实现了一个 JUnit5 测试来验证我们的服务。注意,我们正在创建模拟服务器(`**new** WireMockServer`,并使用 WireMock 在测试设置(`@BeforeEach`中提供的静态方法`stubFor(...)`对 REST 服务调用进行插桩。由于在本例中,SUT 非常简单,并且没有文档,因此我们也在每个测试的设置中直接实例化了类`RemoteFileService`,使用模拟服务器 URL 作为构造函数参数。最后,我们测试我们的服务(使用模拟服务器),在本例中,通过调用方法`getFile`并评估其输出,简单地运行名为`wireMockServer`的对象。
```java
......
......@@ -162,7 +162,7 @@ TDD 的主要目标是可测试的代码设计,将测试作为非常有用的
# 嘲笑
为了让测试快速运行并提供持续的反馈,代码需要以这样一种方式组织:方法、函数和类可以很容易地替换为 mock 和 stub。这种类型的实际代码替换的常用词是**测试双精度**。外部依赖性会严重影响执行速度;例如,我们的代码可能需要与数据库通信。通过模仿外部依赖关系,我们能够大幅提高速度。整个单元测试套件的执行应该以分钟为单位,如果不是秒的话。以一种易于模仿和存根的方式设计代码迫使我们通过应用关注点分离来更好地构建代码。
为了让测试快速运行并提供持续的反馈,代码需要以这样一种方式组织:方法、函数和类可以很容易地替换为 mock 和 stub。这种类型的实际代码替换的常用词是**测试替身**。外部依赖性会严重影响执行速度;例如,我们的代码可能需要与数据库通信。通过模仿外部依赖关系,我们能够大幅提高速度。整个单元测试套件的执行应该以分钟为单位,如果不是秒的话。以一种易于模仿和插桩的方式设计代码迫使我们通过应用关注点分离来更好地构建代码。
比速度更重要的是去除外部因素的好处。设置数据库、web 服务器、外部 API 和代码可能需要的其他依赖项既耗时又不可靠。在许多情况下,这些依赖项甚至可能不可用。例如,我们可能需要创建一个与数据库通信的代码,并让其他人创建一个模式。如果没有 mock,我们将需要等待该模式被设置。
......
......@@ -647,7 +647,7 @@ public class FriendshipsMongo {
# 莫基托
Mockito 是一个 Java 框架,允许轻松创建测试双精度
Mockito 是一个 Java 框架,允许轻松创建测试替身
渐变相关性如下所示:
......
......@@ -57,14 +57,14 @@ date.getTime(); // What is the result this method returns?
**双重测试**是以下所有类型的通用名称:
* 虚拟对象的目的是充当实方法参数的替代品
* 测试存根可用于将真实对象替换为特定于测试的对象,该对象将所需的间接输入馈送到被测系统中
* 测试可用于将真实对象替换为特定于测试的对象,该对象将所需的间接输入馈送到被测系统中
* **Test Spy**捕获被测**系统****SUT**)对另一个组件的间接输出调用,供测试人员稍后验证
* Mock 对象将 SUT 所依赖的对象替换为测试特定的对象,以验证 SUT 是否正确使用该对象
* Fake 对象用更轻的实现替换 SUT 所依赖的组件
如果你感到困惑,它可能会帮助你知道你不是唯一的一个。事情甚至比这更复杂,因为框架或作者之间没有明确的协议,也没有命名标准。术语令人困惑且不一致,前面提到的术语并不是每个人都接受的。
为了简化,在本书中,我们将使用 Mockito(我们选择的框架)使用的相同名称。这样,您将使用的方法将与您将进一步阅读的术语相对应。我们将继续使用模拟作为其他人可能称之为**测试双精度**的通用术语。此外,我们将使用模拟或间谍术语来指代`Mockito`方法。
为了简化,在本书中,我们将使用 Mockito(我们选择的框架)使用的相同名称。这样,您将使用的方法将与您将进一步阅读的术语相对应。我们将继续使用模拟作为其他人可能称之为**测试替身**的通用术语。此外,我们将使用模拟或间谍术语来指代`Mockito`方法。
# 模拟对象
......@@ -710,7 +710,7 @@ public final void before() throws UnknownHostException {
}
```
现在我们已经用我们认为是默认的行为(调用`saveMove`时返回`true`)对模拟集合进行了存根,我们可以继续编写规范:
现在我们已经用我们认为是默认的行为(调用`saveMove`时返回`true`)对模拟集合进行了插桩,我们可以继续编写规范:
```java
@Test
......
......@@ -21,7 +21,7 @@
虽然在**TDD**中有**测试**这个词,但这不是主要的好处,也不是目的。TDD 首先是一种更好的代码设计方法的概念。除此之外,我们还将进行一些测试,这些测试应用于持续检查应用程序是否继续按预期工作。
以前经常提到速度的重要性。虽然我们越来越精通 TDD 可以部分实现这一点,但另一个贡献者是**测试加倍**(模仿、存根、间谍等)。通过这些,我们可以消除对外部依赖关系的需求,如数据库、文件系统、第三方服务等。
以前经常提到速度的重要性。虽然我们越来越精通 TDD 可以部分实现这一点,但另一个贡献者是**测试加倍**(模仿、、间谍等)。通过这些,我们可以消除对外部依赖关系的需求,如数据库、文件系统、第三方服务等。
TDD 的其他好处是什么?文档就是其中之一。由于代码本身是我们正在处理的应用程序的唯一准确且始终是最新的表示形式,因此当我们需要更好地理解一段代码的功能时,使用 TDD 编写的规范(也是代码)是我们应该首先求助的地方。
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册