5.md 110.1 KB
Newer Older
W
wizardforcel 已提交
1
# JUnit5 与外部框架的集成
W
init  
wizardforcel 已提交
2

W
wizardforcel 已提交
3 4 5
如果我比别人看得更远,那就是站在巨人的肩膀上。

——艾萨克·牛顿
W
init  
wizardforcel 已提交
6

W
wizardforcel 已提交
7
如第 2 章所述,JUnit 5 的扩展模型允许我们通过第三方(工具供应商、开发人员等)扩展 JUnit 5 的核心功能。在 Jupiter 扩展模型中,扩展点是一个回调接口,扩展实现该接口,然后在 JUnit 5 框架中注册(激活)。正如我们将在本章中发现的,JUnit5 扩展模型可用于提供与现有第三方框架的无缝集成。具体地说,在本章中,我们将回顾 JUnit 5 扩展的以下技术:
W
init  
wizardforcel 已提交
8 9

*   **Mockito**:Mock(test-double)单元测试框架。
W
wizardforcel 已提交
10
*   **Spring**:用于构建企业应用程序的 Java 框架。
W
wizardforcel 已提交
11
*   **Selenium**:一个自动化 Web 应用程序导航和评估的测试框架。
W
wizardforcel 已提交
12
*   **Cumber**:测试框架,允许我们创建按照**行为驱动开发****BDD**)风格编写的验收测试。
W
init  
wizardforcel 已提交
13 14
*   **Docker**:一种软件技术,允许我们将任何应用程序打包并作为轻量级便携容器运行。

W
wizardforcel 已提交
15
此外,我们发现 JUnit5 扩展模型并不是与外部世界集成的唯一方式。具体而言,我们研究 JUnit 5 如何与以下内容一起使用:
W
init  
wizardforcel 已提交
16

W
wizardforcel 已提交
17 18
*   **Android**(基于 Linux 的移动操作系统):我们可以使用 JUnit5 的 Gradle 插件在 Android 项目中运行 Jupiter 测试。
*   **REST**(设计分布式系统的体系结构风格):我们可以简单地使用第三方库(如 REST Assured 或 WireMock)或使用 Spring 的完全集成方法(与服务实现一起测试)来交互和验证 REST 服务。
W
init  
wizardforcel 已提交
19

W
wizardforcel 已提交
20
# Mockito
W
init  
wizardforcel 已提交
21

W
wizardforcel 已提交
22
[Mockito](http://site.mockito.org/) 是一个面向 Java 的开源模拟单元测试框架,于 2008 年 4 月首次发布。当然,Mockito 不是 Java 的唯一模拟框架;还有其他一些,例如:
W
init  
wizardforcel 已提交
23

W
wizardforcel 已提交
24 25 26 27
*   [EasyMock](http://easymock.org/)
*   [JMock](http://www.jmock.org/)
*   [PowerMock](http://powermock.github.io/)
*   [JMockit](http://jmockit.org/)
W
init  
wizardforcel 已提交
28

W
wizardforcel 已提交
29
我们可以说,在撰写本文时,Mockito 是大多数开发人员和测试人员在 Java 测试中首选的模拟框架。为了证明这一说法的合理性,我们使用了下面的屏幕截图,它显示了从 2004 年到 2017 年,谷歌趋势中[术语 Mockito、EasyMock、JMock、PowerMock 和 JMockit 的演变](https://trends.google.com/)。在这一时期的开始,我们可以看到人们对 EasyMock 和 JMock 产生了极大的兴趣;然而,与其他框架相比,Mockito 的需求更大:
W
init  
wizardforcel 已提交
30 31 32

![](img/00096.jpeg)

W
wizardforcel 已提交
33
谷歌趋势 Mockito、EasyMock、JMock、PowerMock 和 JMockit 的发展
W
init  
wizardforcel 已提交
34

W
wizardforcel 已提交
35
# 简言之,Mockito
W
init  
wizardforcel 已提交
36

W
wizardforcel 已提交
37
正如第一章、“软件质量回顾和 Java 测试”中介绍的,软件测试有不同的层次,如单元、集成、系统或验收。关于单元测试,它们应该为单个软件单独执行,例如,单个类。此级别测试的目标是验证单元的功能,而不是其依赖性。
W
init  
wizardforcel 已提交
38

W
wizardforcel 已提交
39
换句话说,我们想要测试被测的**系统****SUT**),而不是它的**依赖组件****DOC**)。为了实现这种隔离,我们通常使用*测试替身*来替换这些文档。模拟对象是一种替身测试,它是按照对真实文档的期望进行编程的。
W
init  
wizardforcel 已提交
40

W
wizardforcel 已提交
41
简单地说,Mockito 是一个测试框架,允许创建、插桩和验证模拟对象。为此,Mockito 提供了一个 API 来隔离 SUT 及其文档。一般来说,使用 Mockito 包括三个不同的步骤:
W
init  
wizardforcel 已提交
42

W
wizardforcel 已提交
43
1.  **模拟对象**:为了隔离我们的 SUT,我们使用 Mockito API 创建其关联文档的 mock。这样,我们保证 SUT 不依赖于它的实际文档,而我们的单元测试实际上关注于 SUT。
W
wizardforcel 已提交
44
2.  **设置期望值**:mock 对象与其他测试替身对象(如桩)的区别在于,可以根据单元测试的需要,使用自定义期望值对 mock 对象进行编程。Mockito 术语中的这个过程称为 stubing 方法,其中这些方法属于 mock。默认情况下,模拟对象模仿真实对象的行为。实际上,这意味着模拟对象返回适当的伪值,例如布尔类型为`false`,对象为`null`,整数或长返回类型为 0,等等。Mockito 允许我们使用一个丰富的 API 来改变这种行为,它允许桩在调用方法时返回一个特定的值。
W
init  
wizardforcel 已提交
45

W
wizardforcel 已提交
46
当一个模拟对象没有任何预期(即没有*桩方法*时,从技术上讲,它不是*模拟*对象,而是*虚拟*对象(请看第一章、“软件质量和 Java 测试回顾”对于定义)。
W
init  
wizardforcel 已提交
47

W
wizardforcel 已提交
48
3.  **验证**:最后,我们正在创建测试,因此,我们需要对 SUT 进行某种验证。Mockito 提供了一个强大的 API 来执行不同类型的验证。使用此 API,我们评估与 SUT 和文档的交互,使用模拟验证调用顺序,或者捕获并验证传递给桩方法的参数。此外,可以使用 JUnit 的内置断言功能或使用第三方断言库(例如,Hamcrest、AssertJ 或 Truth)来补充 Mockito 的验证功能。参见第 3 章“JUnit 5 标准测试”中的“断言”部分。
W
init  
wizardforcel 已提交
49

W
wizardforcel 已提交
50
下表总结了按上述阶段分组的 Mockito API:
W
init  
wizardforcel 已提交
51 52

| **Mockito API** | **说明** | **阶段** |
W
wizardforcel 已提交
53
| --- | --- | --- |
W
wizardforcel 已提交
54 55
| `@Mock` | 此注释标识由 Mockito 创建的模拟对象。这通常用于单据。 | 1.模拟对象 |
| `@InjectMocks` | 此注释标识将在其中注入模拟的对象。这通常用于我们要测试的单元,即我们的 SUT。 | 1.模拟对象 |
W
wizardforcel 已提交
56
| `@Spy` | 除了 mock 之外,Mockito 还允许我们创建 spy 对象(即部分 mock 实现,因为它们在非桩方法中使用真实实现)。 | 1.模拟对象 |
W
wizardforcel 已提交
57 58 59 60 61 62 63 64
| `Mockito.when(x).thenReturn(y)`| 这些方法允许我们指定给定模拟对象的桩方法(`x`应该返回的值(`y`。 | 2.设定期望值(*插桩方式*) |
| `Mockito.doReturn(y).when(x)` | | |
| `Mockito.when(x).thenThrow(e)` | 这些方法允许我们指定调用给定模拟对象的桩方法(`x`时应引发的异常(`e`)。 | 2.设定期望值(*插桩方式*) |
| `Mockito.doThrow(e).when(x)` | | |
| `Mockito.when(x).thenAnswer(a)` | 与返回硬编码值不同,当调用模拟的给定方法(`x`时,将执行动态用户定义逻辑(`Answer a`)。 | 2.设定期望值(*插桩方式*) |
| `Mockito.doAnswer(a).when(x)` | | |
| `Mockito.when(x).thenCallRealMethod()` | 这个方法允许我们真正实现一个方法,而不是模拟的方法。 | 2.设定期望值(*插桩方式*) |
| `Mockito.doCallRealMethod().when(x)` | | |
W
wizardforcel 已提交
65
| `Mockito.doNothing().when(x)` | 使用间谍时,默认行为是调用对象的实际方法。为了避免执行`void`方法`x`,使用此方法。 | 2.设定期望值(*插桩方式*) |
W
wizardforcel 已提交
66 67 68 69
| `BDDMockito.given(x).willReturn(y)` | 行为驱动开发是一种测试方法,在这种方法中,测试是根据场景指定的,并在`given`(初始上下文)、`when`(事件发生)和`then`(确保某些结果)时实施。Mockito 通过类`BDDMockito`支持这种类型的测试。桩方法(`x`的行为等同于`Mockito.when(x)`。 | 2.设定期望值(*插桩方式*) |
| `BDDMockito.given(x).willThrow(e)` | | |
| `BDDMockito.given(x).willAnswer(a)` | | |
| `BDDMockito.given(x).willCallRealMethod()` | | |
W
wizardforcel 已提交
70 71
| `Mockito.verify()` | 此方法验证模拟对象的调用。可以选择使用以下方法增强此验证: | 3.核查 |
| | `times(n)`:stubbed 方法被精确调用`n`次。 | |
W
wizardforcel 已提交
72
| | `never()`:从未调用桩方法。 | |
W
wizardforcel 已提交
73 74 75 76 77
| | `atLeastOnce()`:stubbed 方法至少调用一次。 | |
| | `atLeast(n)`:stubbed 方法至少被调用 n 次。 | |
| | `atMost(n)`:stubbed 方法最多调用 n 次。 | |
| | `only()`:如果对 mock 对象调用任何其他方法,mock 将失败。 | |
| | `timeout(m)`:此方法最多在`m`毫秒内调用。 | |
W
wizardforcel 已提交
78 79
| `Mockito.verifyZeroInteractions()``Mockito.verifyNoMoreInteractions()` | 这两个方法验证桩方法没有交互。在内部,它们使用相同的实现。 | 3.核查 |
| `@Captor` | 这个注释允许我们定义一个`ArgumentChaptor`对象,目的是验证传递给桩方法的参数。 | 3.核查 |
W
init  
wizardforcel 已提交
80 81
| `Mockito.inOrder` | 它有助于验证与模拟的交互是否按给定顺序执行。 | 3.核查 |

W
wizardforcel 已提交
82
使用上表中描述的不同注释(`@Mock``@InjectMocks``@Spy``@Captor`)是可选的,尽管对于测试可读性的影响是推荐的。换句话说,除了使用不同的 Mockito 类使用注释外,还有其他选择。例如,为了创建一个`Mock`,我们可以使用注释`@Mock`,如下所示:
W
init  
wizardforcel 已提交
83

W
wizardforcel 已提交
84
```java
W
init  
wizardforcel 已提交
85 86 87 88 89 90
@Mock
MyDoc docMock;
```

替代方法是使用`Mockito.mock`方法,如下所示:

W
wizardforcel 已提交
91
```java
W
wizardforcel 已提交
92
MyDoc docMock = Mockito.mock(MyDoc.class)
W
init  
wizardforcel 已提交
93 94
```

W
wizardforcel 已提交
95
以下各节包含使用上表中描述的 Jupiter 测试中的 Mockito API 的综合示例。
W
init  
wizardforcel 已提交
96

W
wizardforcel 已提交
97
# Mockito 的 JUnit5 扩展
W
init  
wizardforcel 已提交
98

W
wizardforcel 已提交
99
在撰写本文时,还没有官方的 JUnit5 扩展在 Jupiter 测试中使用 Mockito。然而,JUnit5 团队提供了一个简单的现成 Java 类,实现了一个简单但有效的 Mockito 扩展。该类可以在 [JUnit5 用户指南](http://junit.org/junit5/docs/current/user-guide/)中找到,其代码如下:
W
init  
wizardforcel 已提交
100

W
wizardforcel 已提交
101
```java
W
wizardforcel 已提交
102
import static org.mockito.Mockito.mock;
W
init  
wizardforcel 已提交
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

import java.lang.reflect.Parameter;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class MockitoExtension
        implements TestInstancePostProcessor, ParameterResolver {

    @Override
    public void postProcessTestInstance(Object testInstance,
            ExtensionContext context) {
W
wizardforcel 已提交
119
        MockitoAnnotations.initMocks(testInstance);
W
init  
wizardforcel 已提交
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
    }

    @Override
    public boolean supportsParameter(ParameterContext parameterContext,
       ExtensionContext extensionContext) {
      return 
       parameterContext.getParameter().isAnnotationPresent(Mock.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext,
            ExtensionContext extensionContext) {
        return getMock(parameterContext.getParameter(), extensionContext);
    }

    private Object getMock(Parameter parameter,
            ExtensionContext extensionContext) {
        Class<?> mockType = parameter.getType();
        Store mocks = extensionContext
W
wizardforcel 已提交
139
                .getStore(Namespace.create(MockitoExtension.class, 
W
init  
wizardforcel 已提交
140 141 142 143
                mockType));
        String mockName = getMockName(parameter);
        if (mockName != null) {
            return mocks.getOrComputeIfAbsent(mockName,
W
wizardforcel 已提交
144
                    key -> mock(mockType, mockName));
W
init  
wizardforcel 已提交
145 146
        } else {
            return mocks.getOrComputeIfAbsent(mockType.getCanonicalName(),
W
wizardforcel 已提交
147
                    key -> mock(mockType));
W
init  
wizardforcel 已提交
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
        }
    }

    private String getMockName(Parameter parameter) {
        String explicitMockName = 
                parameter.getAnnotation(Mock.class).name()
                .trim();
        if (!explicitMockName.isEmpty()) {
            return explicitMockName;
        } else if (parameter.isNamePresent()) {
            return parameter.getName();
        }
        return null;
    }

}
```

W
wizardforcel 已提交
166
该扩展(以及其他扩展)计划在开源项目 [JUnit Pioneer](http://junit-pioneer.org/) 中发布。该项目由 Java 开发者、博客 [CodeFX](https://blog.codefx.org/) 的作者 Nicolai Palog 负责维护。
W
init  
wizardforcel 已提交
167

W
wizardforcel 已提交
168
检查前面的类,我们可以检查它是否只是 Jupiter 扩展模型的一个用例(在本书的第 2 章、“JUnit 5 中的新增内容”中描述),它实现了扩展回调`TestInstancePostProcessor``ParameterResolver`。首先,在测试用例实例化后,调用`postProcessTestInstance`方法,并在该方法的主体中进行 mock 的初始化:
W
init  
wizardforcel 已提交
169

W
wizardforcel 已提交
170
```java
W
wizardforcel 已提交
171
MockitoAnnotations.initMocks(testInstance)
W
init  
wizardforcel 已提交
172 173
```

W
wizardforcel 已提交
174
这与在 Mockito 中使用 JUnit4 runner 的效果相同:`@RunWith(MockitoJUnitRunner.class)`
W
init  
wizardforcel 已提交
175 176 177

此外,该扩展还实现了接口`ParameterResolver`。这意味着在注册扩展(`@ExtendWith(MockitoExtension.class)`的测试中允许方法级的依赖项注入。特别是,注释将为带有`@Mock`注释的测试参数(位于包`org.mockito`中)注入模拟对象。

W
wizardforcel 已提交
178
让我们看一些例子来说明这个扩展和 Mockito 的用法。与往常一样,我们可以在 [GitHub 存储库](https://github.com/bonigarcia/mastering-junit5)上找到此示例的源代码。上述扩展(`MockitoExtension`的副本包含在项目`junit5-mockito`中。为了指导这些示例,我们在软件应用程序中实现了一个典型的用例:用户在软件系统中的登录。
W
init  
wizardforcel 已提交
179 180 181 182 183

在本用例中,我们假设用户与由三个类组成的系统交互:

*   `LoginController`:接收用户请求并返回响应的类。此请求被发送到`LoginService`组件。
*   `LoginService`:此类实现用例的功能。为此,需要确认用户是否在系统中经过身份验证。为此,它需要读取在`LoginRepository`类中实现的持久层。
W
wizardforcel 已提交
184
*   `LoginRepository`:此类允许访问系统的持久层,通常通过数据库实现。此类也可以称为**数据访问对象****DAO**)。
W
init  
wizardforcel 已提交
185 186 187 188 189 190 191 192 193 194 195 196 197

就构成而言,这三类之间的关系如下:

![](img/00097.jpeg)

登录用例类图(类之间的组合关系)

用例中涉及的两个基本操作(登录和注销)的序列图如下图所示:

![](img/00098.jpeg)

登录用例序列图

W
wizardforcel 已提交
198
我们使用几个简单的 Java 类来实现这个示例。首先,`LoginController`按组合使用`LoginService`
W
init  
wizardforcel 已提交
199

W
wizardforcel 已提交
200
```java
W
init  
wizardforcel 已提交
201 202 203 204 205 206
package io.github.bonigarcia;

public class LoginController {
    public LoginService loginService = new LoginService();

    public String login(UserForm userForm) {
W
wizardforcel 已提交
207
        System.out.println("LoginController.login " + userForm);
W
init  
wizardforcel 已提交
208 209 210 211 212 213 214 215 216 217 218 219 220 221
        try {
            if (userForm == null) {
                return "ERROR";
            } else if (loginService.login(userForm)) {
                return "OK";
            } else {
                return "KO";
            }
        } catch (Exception e) {
            return "ERROR";
        }
    }

    public void logout(UserForm userForm) {
W
wizardforcel 已提交
222
        System.out.println("LoginController.logout " + userForm);
W
init  
wizardforcel 已提交
223 224 225 226 227
        loginService.logout(userForm);
    }
}
```

W
wizardforcel 已提交
228
`UserForm`对象是一个简单的 Java 类,有时称为**普通旧 Java 对象****POJO**),有两个属性用户名和密码:
W
init  
wizardforcel 已提交
229

W
wizardforcel 已提交
230
```java
W
init  
wizardforcel 已提交
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
package io.github.bonigarcia;

public class UserForm {

    public String username;
    public String password;

    public UserForm(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Getters and setters

    @Override
    public String toString() {
        return "UserForm [username=" + username + ", password=" + password
                + "]";
    }
}
```

W
wizardforcel 已提交
253
然后,服务依赖于存储库(`LoginRepository`进行数据访问。在本例中,服务还使用 Java 列表实现了一个用户注册表,其中存储了经过身份验证的用户:
W
init  
wizardforcel 已提交
254

W
wizardforcel 已提交
255
```java
W
init  
wizardforcel 已提交
256 257 258 259 260 261 262 263 264 265 266
package io.github.bonigarcia;

import java.util.ArrayList;
import java.util.List;

public class LoginService {

    private LoginRepository loginRepository = new LoginRepository();
    private List<String> usersLogged = new ArrayList<>();

    public boolean login(UserForm userForm) {
W
wizardforcel 已提交
267
        System.out.println("LoginService.login " + userForm);
W
init  
wizardforcel 已提交
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288

        // Preconditions
        checkForm(userForm);

        // Same user cannot be logged twice
        String username = userForm.getUsername();
        if (usersLogged.contains(username)) {
            throw new LoginException(username + " already logged");
        }

        // Call to repository to make logic
        boolean login = loginRepository.login(userForm);

        if (login) {
            usersLogged.add(username);
        }

        return login;
    }

    public void logout(UserForm userForm) {
W
wizardforcel 已提交
289
        System.out.println("LoginService.logout " + userForm);
W
init  
wizardforcel 已提交
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315

        // Preconditions
        checkForm(userForm);

        // User should be logged beforehand
        String username = userForm.getUsername();
        if (!usersLogged.contains(username)) {
            throw new LoginException(username + " not logged");
        }

        usersLogged.remove(username);
    }

    public int getUserLoggedCount() {
        return usersLogged.size();
    }

    private void checkForm(UserForm userForm) {
        assert userForm != null;
        assert userForm.getUsername() != null;
        assert userForm.getPassword() != null;
    }

}
```

W
wizardforcel 已提交
316
最后,`LoginRepository`如下所示。为了简单起见,该组件不访问真实数据库,而是实现了一个映射,其中存储了系统假设用户的凭据(其中,`key`为用户名,`value`为密码):
W
init  
wizardforcel 已提交
317

W
wizardforcel 已提交
318
```java
W
init  
wizardforcel 已提交
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
package io.github.bonigarcia;

import java.util.HashMap;
import java.util.Map;

public class LoginRepository {

    Map<String, String> users;

    public LoginRepository() {
        users = new HashMap<>();
        users.put("user1", "p1");
        users.put("user2", "p3");
        users.put("user3", "p4");
    }

    public boolean login(UserForm userForm) {
W
wizardforcel 已提交
336
        System.out.println("LoginRepository.login " + userForm);
W
init  
wizardforcel 已提交
337 338 339 340 341 342 343 344 345
        String username = userForm.getUsername();
        String password = userForm.getPassword();
        return users.keySet().contains(username)
                && users.get(username).equals(password);
    }

}
```

W
wizardforcel 已提交
346
现在,我们将使用 JUnit5 和 Mockito 测试我们的系统。首先,我们测试控制器组件。因为我们正在进行单元测试,所以需要将`LoginController`登录与系统的其余部分隔离开来。要做到这一点,我们需要模拟它的依赖关系,在本例中是`LoginService`组件。使用前面解释的 SUT/DOC 术语,在这个测试中,我们的 SUT 是类`LoginController`,它的 DOC 是类`LoginService`
W
init  
wizardforcel 已提交
347

W
wizardforcel 已提交
348
为了用 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`
W
init  
wizardforcel 已提交
349

W
wizardforcel 已提交
350
```java
W
init  
wizardforcel 已提交
351 352
package io.github.bonigarcia;

W
wizardforcel 已提交
353
import static org.junit.jupiter.api.Assertions.assertEquals;
W
wizardforcel 已提交
354 355 356 357
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
W
init  
wizardforcel 已提交
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import io.github.bonigarcia.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class LoginControllerLoginTest {

    // Mocking objects
    @InjectMocks
    LoginController loginController;

    @Mock
    LoginService loginService;

    // Test data
    UserForm userForm = new UserForm("foo", "bar");

    @Test
    void testLoginOk() {
        // Setting expectations (stubbing methods)
W
wizardforcel 已提交
381
        when(loginService.login(userForm)).thenReturn(true);
W
init  
wizardforcel 已提交
382 383 384 385 386

        // Exercise SUT
        String reseponseLogin = loginController.login(userForm);

        // Verification
W
wizardforcel 已提交
387
        assertEquals("OK", reseponseLogin);
W
wizardforcel 已提交
388 389
        verify(loginService).login(userForm);
        verifyNoMoreInteractions(loginService);
W
init  
wizardforcel 已提交
390 391 392 393 394
    }

    @Test
    void testLoginKo() {
        // Setting expectations (stubbing methods)
W
wizardforcel 已提交
395
        when(loginService.login(userForm)).thenReturn(false);
W
init  
wizardforcel 已提交
396 397 398 399 400

        // Exercise SUT
        String reseponseLogin = loginController.login(userForm);

        // Verification
W
wizardforcel 已提交
401
        assertEquals("KO", reseponseLogin);
W
wizardforcel 已提交
402 403
        verify(loginService).login(userForm);
        verifyZeroInteractions(loginService);
W
init  
wizardforcel 已提交
404 405 406 407 408
    }

}
```

W
wizardforcel 已提交
409
如果执行此测试,只需检查标准输出上的跟踪,就可以检查 SUT 是否已实际执行。此外,我们保证两项测试的验证阶段均已成功,因为两项测试均已通过:
W
init  
wizardforcel 已提交
410 411 412

![](img/00099.gif)

W
wizardforcel 已提交
413
使用 JUnit 5 和 Mockito 执行`LoginControllerLoginTest`的单元测试
W
init  
wizardforcel 已提交
414

W
wizardforcel 已提交
415
现在让我们来看另一个例子,在这个例子中,对组件`LoginController`测试了负面场景(即错误情况)。下面的类包含两个测试,第一个(`testLoginError`)用于评估使用空表单时系统的响应(应该是`ERROR`)。在第二个测试(`testLoginException`中,我们对模拟`loginService`的方法登录进行编程,以在首次使用任何表单时引发异常。然后,我们练习 SUT(`LoginController`并评估该响应实际上是一个`ERROR`
W
init  
wizardforcel 已提交
416

W
wizardforcel 已提交
417
注意,在设置 mock 方法的期望值时,我们使用的是参数匹配器 any(由 Mockito 提供)。
W
init  
wizardforcel 已提交
418

W
wizardforcel 已提交
419
```java
W
init  
wizardforcel 已提交
420 421
package io.github.bonigarcia;

W
wizardforcel 已提交
422
import static org.junit.jupiter.api.Assertions.assertEquals;
W
wizardforcel 已提交
423 424
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
W
init  
wizardforcel 已提交
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import io.github.bonigarcia.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class LoginControllerErrorTest {

    @InjectMocks
    LoginController loginController;

    @Mock
    LoginService loginService;

    UserForm userForm = new UserForm("foo", "bar");

    @Test
    void testLoginError() {
        // Exercise
        String response = loginController.login(null);

        // Verify
W
wizardforcel 已提交
449
        assertEquals("ERROR", response);
W
init  
wizardforcel 已提交
450 451 452 453 454
    }

    @Test
    void testLoginException() {
        // Expectation
W
wizardforcel 已提交
455
        when(loginService.login(any(UserForm.class)))
W
init  
wizardforcel 已提交
456 457 458 459 460 461
                .thenThrow(IllegalArgumentException.class);

        // Exercise
        String response = loginController.login(userForm);

        // Verify
W
wizardforcel 已提交
462
        assertEquals("ERROR", response);
W
init  
wizardforcel 已提交
463 464 465 466 467
    }

}
```

W
wizardforcel 已提交
468
同样,在 Shell 中运行测试时,我们可以确认这两个测试都已正确执行,并且已执行 SUT:
W
init  
wizardforcel 已提交
469 470 471

![](img/00100.gif)

W
wizardforcel 已提交
472
使用 JUnit 5 和 Mockito 执行`LoginControllerErrorTest`的单元测试
W
init  
wizardforcel 已提交
473

W
wizardforcel 已提交
474
让我们看一个使用 BDD 样式的示例。为了达到这个目的,使用了`BDDMockito`类。请注意,示例中导入了该类的静态方法。然后,实现了四个测试。事实上,这四个测试与前面的示例(`LoginControllerLoginTest``LoginControllerErrorTest`中实现的测试完全相同,但这次使用的是 BDD 样式和更紧凑的样式(一行命令)。
W
init  
wizardforcel 已提交
475

W
wizardforcel 已提交
476
```java
W
init  
wizardforcel 已提交
477 478
package io.github.bonigarcia;

W
wizardforcel 已提交
479
import static org.junit.jupiter.api.Assertions.assertEquals;
W
wizardforcel 已提交
480 481
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
W
init  
wizardforcel 已提交
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import io.github.bonigarcia.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class LoginControllerBDDTest {

    @InjectMocks
    LoginController loginController;

    @Mock
    LoginService loginService;

    UserForm userForm = new UserForm("foo", "bar");

    @Test
    void testLoginOk() {
W
wizardforcel 已提交
502
        given(loginService.login(userForm)).willReturn(true);
W
wizardforcel 已提交
503
        assertEquals("OK", loginController.login(userForm));
W
init  
wizardforcel 已提交
504 505 506 507
    }

    @Test
    void testLoginKo() {
W
wizardforcel 已提交
508
        given(loginService.login(userForm)).willReturn(false);
W
wizardforcel 已提交
509
        assertEquals("KO", loginController.login(userForm));
W
init  
wizardforcel 已提交
510 511 512 513
    }

    @Test
    void testLoginError() {
W
wizardforcel 已提交
514
        assertEquals("ERROR", loginController.login(null));
W
init  
wizardforcel 已提交
515 516 517 518
    }

    @Test
    void testLoginException() {
W
wizardforcel 已提交
519
        given(loginService.login(any(UserForm.class)))
W
init  
wizardforcel 已提交
520
                .willThrow(IllegalArgumentException.class);
W
wizardforcel 已提交
521
        assertEquals("ERROR", loginController.login(userForm));
W
init  
wizardforcel 已提交
522 523 524 525 526 527 528 529 530
    }

}
```

该测试类的执行假设执行了四个测试。如下面的屏幕截图所示,它们全部通过:

![](img/00101.gif)

W
wizardforcel 已提交
531
使用 JUnit 5 和 Mockito 执行`LoginControllerBDTest`的单元测试
W
init  
wizardforcel 已提交
532

W
wizardforcel 已提交
533
现在让我们转到系统的下一个组件:`LoginService`。在下面的示例中,我们旨在对该组件进行单元测试,因此首先使用注释`@InjectMocks`在测试中注入 SUT。然后,使用注释`@Mock`模拟文档(`LoginRepository`。该类包含三个测试。第一个(`testLoginOk`用于在收到正确的表单时验证 SUT 的答案。第二个测试(`testLoginKo`验证了相反的场景。最后,第三个测试还验证了系统的错误情况。此服务的实现会记录用户的注册表,并且不允许同一用户登录两次。因此,我们实施了一个测试(`testLoginTwice`),验证当同一用户尝试登录两次时是否引发异常`LoginException`
W
init  
wizardforcel 已提交
534

W
wizardforcel 已提交
535
```java
W
init  
wizardforcel 已提交
536 537
package io.github.bonigarcia;

W
wizardforcel 已提交
538 539 540
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
W
wizardforcel 已提交
541 542 543 544 545
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
W
init  
wizardforcel 已提交
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import io.github.bonigarcia.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class LoginServiceTest {

    @InjectMocks
    LoginService loginService;

    @Mock
    LoginRepository loginRepository;

    UserForm userForm = new UserForm("foo", "bar");

    @Test
    void testLoginOk() {
W
wizardforcel 已提交
566
        when(loginRepository.login(any(UserForm.class))).thenReturn(true);
W
wizardforcel 已提交
567
        assertTrue(loginService.login(userForm));
W
wizardforcel 已提交
568
        verify(loginRepository, atLeast(1)).login(userForm);
W
init  
wizardforcel 已提交
569 570 571 572
    }

    @Test
    void testLoginKo() {
W
wizardforcel 已提交
573
        when(loginRepository.login(any(UserForm.class))).thenReturn(false);
W
wizardforcel 已提交
574
        assertFalse(loginService.login(userForm));
W
wizardforcel 已提交
575
        verify(loginRepository, times(1)).login(userForm);
W
init  
wizardforcel 已提交
576 577 578 579
    }

    @Test
    void testLoginTwice() {
W
wizardforcel 已提交
580
        when(loginRepository.login(userForm)).thenReturn(true);
W
wizardforcel 已提交
581
        assertThrows(LoginException.class, () -> {
W
init  
wizardforcel 已提交
582 583 584 585 586 587 588 589
            loginService.login(userForm);
            loginService.login(userForm);
        });
    }

}
```

W
wizardforcel 已提交
590
和往常一样,在 Shell 中执行测试可以让我们了解事情的进展情况。我们可以检查登录服务是否已经运行了四次(因为在第三次测试中,我们执行了两次)。但由于`LoginException`是预期的,该测试成功(以及其他两项):
W
init  
wizardforcel 已提交
591 592 593

![](img/00102.gif)

W
wizardforcel 已提交
594
使用 JUnit 5 和 Mockito 执行`LoginServiceTest`的单元测试
W
init  
wizardforcel 已提交
595

W
wizardforcel 已提交
596
下面的类提供了一个捕获模拟对象参数的简单示例。我们定义了一个类型为`ArgumentCaptor<UserForm>`的类属性,该类属性用`@Captor`注释。然后,在测试主体中,执行 SUT(本例中为`LoginService`,并捕获方法 login 的参数。最后,评估该论点的价值:
W
init  
wizardforcel 已提交
597

W
wizardforcel 已提交
598
```java
W
init  
wizardforcel 已提交
599 600
package io.github.bonigarcia;

W
wizardforcel 已提交
601
import static org.junit.jupiter.api.Assertions.assertEquals;
W
wizardforcel 已提交
602
import static org.mockito.Mockito.verify;
W
init  
wizardforcel 已提交
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import io.github.bonigarcia.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class LoginServiceChaptorTest {

    @InjectMocks
    LoginService loginService;

    @Mock
    LoginRepository loginRepository;

    @Captor
    ArgumentCaptor<UserForm> argCaptor;

    UserForm userForm = new UserForm("foo", "bar");

    @Test
    void testArgumentCaptor() {
        loginService.login(userForm);
W
wizardforcel 已提交
629
        verify(loginRepository).login(argCaptor.capture());
W
wizardforcel 已提交
630
        assertEquals(userForm, argCaptor.getValue());
W
init  
wizardforcel 已提交
631 632 633 634 635
    }

}
```

W
wizardforcel 已提交
636
再次,在控制台中,我们检查 SUT 是否已执行,测试是否声明为成功:
W
init  
wizardforcel 已提交
637 638 639

![](img/00103.gif)

W
wizardforcel 已提交
640
使用 JUnit 5 和 Mockito 执行`LoginServiceChaptorTest`的单元测试
W
init  
wizardforcel 已提交
641

W
wizardforcel 已提交
642
我们在本章中看到的最后一个与 Mockito 有关的例子与间谍的使用有关。正如前面介绍的,默认情况下,间谍在非桩方法中使用实际实现。因此,如果我们不在 spy 对象中桩方法,我们得到的就是测试中的真实对象。这就是下一个示例中发生的情况。如我们所见,我们使用`LoginService`作为我们的 SUT,然后我们监视对象`LoginRepository`。由于在测试主体中,我们没有在 spy 对象中编程期望,因此我们正在测试中评估真实系统。
W
init  
wizardforcel 已提交
643 644 645

总之,测试数据准备好获得正确的登录(使用用户名为`user`,密码为`p1`,这在`LoginRepository`的实际实现中存在于硬编码值中),然后获得一些不成功登录的伪值:

W
wizardforcel 已提交
646
```java
W
init  
wizardforcel 已提交
647 648
package io.github.bonigarcia;

W
wizardforcel 已提交
649 650
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
W
init  
wizardforcel 已提交
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
 import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Spy;
import io.github.bonigarcia.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class LoginServiceSpyTest {

    @InjectMocks
    LoginService loginService;

    @Spy
    LoginRepository loginRepository;

    UserForm userOk = new UserForm("user1", "p1");
    UserForm userKo = new UserForm("foo", "bar");

    @Test
    void testLoginOk() {
W
wizardforcel 已提交
671
        assertTrue(loginService.login(userOk));
W
init  
wizardforcel 已提交
672 673 674 675
    }

    @Test
    void testLoginKo() {
W
wizardforcel 已提交
676
        assertFalse(loginService.login(userKo));
W
init  
wizardforcel 已提交
677 678 679 680
    }
}
```

W
wizardforcel 已提交
681
在 Shell 中,我们可以检查两个测试是否正确执行,在这种情况下,实际组件(包括`LoginService``LoginRepository`)实际执行了:
W
init  
wizardforcel 已提交
682 683 684

![](img/00104.gif)

W
wizardforcel 已提交
685
使用 JUnit 5 和 Mockito 执行`LoginServicesByTest`的单元测试
W
init  
wizardforcel 已提交
686

W
wizardforcel 已提交
687
这些示例演示了 Mockito 的一些功能,当然不是全部。欲了解更多信息,请访问[官方 Mockito 参考资料](http://site.mockito.org/)
W
init  
wizardforcel 已提交
688

W
wizardforcel 已提交
689
# Spring
W
init  
wizardforcel 已提交
690

W
wizardforcel 已提交
691
[Spring](https://spring.io/) 是一个用于构建企业应用程序的开源 Java 框架。2002 年 10 月,Rod Johnson 与他的书《专家一对一 J2EE 设计和开发》一起首次撰写了这本书。Spring 最初的动机是摆脱 J2EE 的复杂性,提供一个轻量级的基础设施,旨在使用简单的 pojo 作为构建块简化企业应用程序的开发。
W
init  
wizardforcel 已提交
692 693 694

# 一言以蔽之

W
wizardforcel 已提交
695
Spring 框架的核心技术被称为**控制反转****IoC**),这是在实际使用这些对象的类之外实例化对象的过程。这些对象在 Spring 行话中称为 Bean 或组件,默认情况下创建为*单例*对象。负责创建 Bean 的实体称为 SpringIOC 容器。这是通过**依赖项注入****DI**)实现的,这是提供一个对象的依赖项而不是自己构建它们的过程。
W
init  
wizardforcel 已提交
696

W
wizardforcel 已提交
697
IoC 和 DI 经常互换使用。然而,如前一段所述,这些概念并不完全相同(IoC 是通过 DI 实现的)。
W
init  
wizardforcel 已提交
698

W
wizardforcel 已提交
699
如本节下一部分所述,Spring 是一个模块化框架。`spring-context`模块中提供了 Spring 的核心功能(即 IoC)。该模块提供创建**应用上下文**的能力,即 Spring 的 DI 容器。在 Spring 中有许多不同的方法来定义应用程序上下文。以下是两种最重要的类型:
W
init  
wizardforcel 已提交
700

W
wizardforcel 已提交
701
*   `AnnotationConfigApplicationContext`:应用程序上下文,它接受带注释的类来标识要在容器中执行的 SpringBean。在这种类型的上下文中,Bean 通过使用注释`@Component`注释普通类来标识。它不是唯一一个将类声明为 SpringBean 的类。还有更多的原型注释:`@Controller`(表示层原型,用于 Web 模块,MVC)、`@Repository`(持久层原型,用于数据访问模块,称为 Spring 数据)和`@Service`(用于服务层)。这三个注释用于分离应用程序的各个层。最后,使用`@Configuration`注释的类允许通过使用`@Bean`注释方法来定义 Springbeans(这些方法返回的对象将是容器中的 Springbeans):
W
init  
wizardforcel 已提交
702 703 704

![](img/00105.jpeg)

W
wizardforcel 已提交
705
用于定义 Bean 的 Spring 原型
W
init  
wizardforcel 已提交
706

W
wizardforcel 已提交
707
*   `ClassPathXmlApplicationContext`:应用程序上下文,它接受在项目类路径中的 XML 文件中声明的 Bean 定义。
W
init  
wizardforcel 已提交
708

W
wizardforcel 已提交
709
Spring2.5 引入了基于注释的上下文配置。springioc 容器与配置元数据(即 Bean 定义)的实际写入格式完全解耦。如今,许多开发人员选择基于注释的配置,而不是基于 XML 的配置。因此,在本书中,我们将在示例中仅使用基于注释的上下文配置。
W
init  
wizardforcel 已提交
710

W
wizardforcel 已提交
711
让我们看一个简单的例子。首先,我们需要在我们的项目中包含`spring-context`依赖项。例如,作为 Maven 依赖项:
W
init  
wizardforcel 已提交
712

W
wizardforcel 已提交
713
```java
W
init  
wizardforcel 已提交
714 715 716 717 718 719 720
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${spring-context.version}</version>
</dependency>
```

W
wizardforcel 已提交
721
然后,我们创建一个可执行的 Java 类(即,使用 main 方法)。注意,在这个类中,类级别有一个注释:`@ComponentScan`。这是 Spring 中非常重要的注释,因为它允许声明 Spring 将在其中以注释的形式查找 Bean 定义的包。如果未定义特定的包(如示例中所示),则将从声明此注释的类的包(在示例中为包`io.github.bonigarcia`)进行扫描。在 main 方法的主体中,我们使用`AnnotationConfigApplicationContext`创建 Spring 应用程序上下文。从该上下文中,我们得到了类为`MessageComponent`的 Spring 组件,并将其`getMessage()`方法的结果写入标准输出:
W
init  
wizardforcel 已提交
722

W
wizardforcel 已提交
723
```java
W
init  
wizardforcel 已提交
724 725 726 727 728 729 730 731 732 733 734 735 736 737
package io.github.bonigarcia;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MySpringApplication {

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new 
                AnnotationConfigApplicationContext(
                MySpringApplication.class)) {
            MessageComponent messageComponent = context
                    .getBean(MessageComponent.class);
W
wizardforcel 已提交
738
            System.out.println(messageComponent.getMessage());
W
init  
wizardforcel 已提交
739 740 741 742 743 744
        }
    }

}
```

W
wizardforcel 已提交
745
bean`MessageComponent`在下面的类中定义。请注意,它只是在类级别使用注释`@Component`声明为 Spring 组件。然后,在本例中,我们使用类构造函数注入另一个名为`MessageService`的 Spring 组件:
W
init  
wizardforcel 已提交
746

W
wizardforcel 已提交
747
```java
W
init  
wizardforcel 已提交
748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768
package io.github.bonigarcia;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MessageComponent {

    private MessageService messageService;

    public MessageComponent(MessageService messageService) {
       this.messageService = messageService;
    }

    public String getMessage() {
        return messageService.getMessage();
    }

}
```

W
wizardforcel 已提交
769
此时,值得回顾一下执行 Spring 组件依赖项注入的不同方式:
W
init  
wizardforcel 已提交
770

W
wizardforcel 已提交
771
1.  字段注入:注入的组件是一个带有`@Autowired`注释的类字段,就像前面的示例一样。作为一个好处,这种注入可以消除杂乱的代码,例如设置器方法或构造函数参数。
W
wizardforcel 已提交
772
2.  Setter 注入:注入的组件被声明为类中的一个字段,然后为该字段创建一个 Setter 并用`@Autowired`注释。
W
init  
wizardforcel 已提交
773

W
wizardforcel 已提交
774
3.  构造函数注入:在类构造函数中注入依赖项,并用`@Autowired`(图中为 3-a)注释。这是前面示例中显示的方式。从 Spring 4.3 开始,不再需要用`@Autowired`注释构造函数来执行注入(3-b)。
W
init  
wizardforcel 已提交
775 776 777 778 779

最新的注入方式(*3-b*)有几个好处,例如在不需要反射机制的情况下提高可测试性(例如,通过模拟库实现)。此外,它可以让开发人员考虑类的设计,因为许多注入的依赖项假设了许多构造函数参数,这应该避免(*上帝对象*反模式)。

![](img/00106.jpeg)

W
wizardforcel 已提交
780
Spring 中依赖项注入(自动连线)的不同方式
W
init  
wizardforcel 已提交
781

W
wizardforcel 已提交
782
我们示例中的最后一个组件名为`MessageService`。请注意,这也是一个 Spring 组件,这次用`@Service`注释以说明其服务性质(从功能角度来看,这与用`@Component`注释类相同):
W
init  
wizardforcel 已提交
783

W
wizardforcel 已提交
784
```java
W
init  
wizardforcel 已提交
785 786 787 788 789 790 791 792 793 794 795 796 797 798
package io.github.bonigarcia;

import org.springframework.stereotype.Service;

@Service
public class MessageService {

    public String getMessage() {
        return "Hello world!";
    }

}
```

W
wizardforcel 已提交
799
现在,如果我们执行本例的主类(称为`MySpringApplication`,请参见此处的源代码),我们将创建一个基于注释的应用程序上下文,并尝试使用资源(这样,应用程序上下文将在最后关闭)。Spring IoC 容器将创建两个 Bean:`MessageService``MessageComponet`。使用应用程序上下文,我们寻找 Bean`MessageComponet`并调用其方法`getMessage`,最终写入标准输出:
W
init  
wizardforcel 已提交
800

W
wizardforcel 已提交
801
```java
W
init  
wizardforcel 已提交
802 803 804 805 806 807 808 809 810 811 812 813 814 815
package io.github.bonigarcia;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MySpringApplication {

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new 
                AnnotationConfigApplicationContext(
                MySpringApplication.class)) {
            MessageComponent messageComponent = context
                    .getBean(MessageComponent.class);
W
wizardforcel 已提交
816
            System.out.println(messageComponent.getMessage());
W
init  
wizardforcel 已提交
817 818 819 820 821 822
        }
    }

}
```

W
wizardforcel 已提交
823
# Spring模块
W
init  
wizardforcel 已提交
824

W
wizardforcel 已提交
825
Spring 框架是模块化的,允许开发人员只使用框架提供的所需模块。此模块的完整列表可在[这里](https://spring.io/projects)找到。下表总结了一些最重要的问题:
W
init  
wizardforcel 已提交
826

W
wizardforcel 已提交
827 828
| **Spring 工程** | **标识** | **说明** |
| --- | --- | --- |
W
wizardforcel 已提交
829
| Spring 框架 | ![](img/00107.jpeg) | 为 DI、事务管理、Web 应用程序(Spring MCV)、数据访问、消息传递等提供核心支持。 |
W
wizardforcel 已提交
830 831 832 833 834 835 836
| Spring IO 平台 | ![](img/00108.jpeg) | 将核心 SpringAPI 整合到一个内聚的、版本化的基础平台中,用于现代应用程序。 |
| SpringBoot | ![](img/00109.jpeg) | 以最少的配置简化了基于 Spring 的独立生产级应用程序的创建。它遵循传统的配置方法。 |
| SpringData | ![](img/00110.jpeg) | 通过使用关系数据库、NoSQL、map reduce 算法等全面的 API 简化数据访问。 |
| SpringCloud | ![](img/00111.jpeg) | 提供一组库和通用模式,用于构建和部署分布式系统和微服务。 |
| SpringSecurity | ![](img/00112.jpeg) | 为基于 Spring 的应用程序提供可定制的身份验证和授权功能。 |
| Spring 集成 | ![](img/00113.jpeg) | 为基于 Spring 的应用程序提供轻量级、基于 POJO 的消息传递,以便与外部系统集成。 |
| SpringBatch | ![](img/00114.jpeg) | 提供一个轻量级框架,旨在为企业系统的操作开发健壮的批处理应用程序。 |
W
init  
wizardforcel 已提交
837

W
wizardforcel 已提交
838
# Spring试验简介
W
init  
wizardforcel 已提交
839

W
wizardforcel 已提交
840
Spring 是一个名为`spring-test`的模块,支持 Spring 组件的单元测试和集成测试。在其他特性中,该模块提供了创建用于测试目的的 Spring 应用程序上下文的能力,或者创建用于单独测试代码的模拟对象的能力。有不同的注释支持此测试功能。最重要的一项清单如下:
W
init  
wizardforcel 已提交
841

W
wizardforcel 已提交
842
*   `@ContextConfiguration`:此注释用于确定如何为集成测试加载和配置`ApplicationContext`。例如,它允许从注释类(使用元素类)或 XML 文件中声明的 Bean 定义(使用元素位置)加载应用程序上下文。
W
init  
wizardforcel 已提交
843 844
*   `@ActiveProfiles`:此注释用于指示容器在应用程序上下文加载期间应激活哪些定义配置文件(例如,开发和测试配置文件)。
*   `@TestPropertySource`:此注释用于配置属性文件的位置和要添加的内联属性。
W
wizardforcel 已提交
845
*   `@WebAppConfiguration`:此注释用于指示Spring上下文`ApplicationContext`加载的是`WebApplicationContext.`
W
init  
wizardforcel 已提交
846 847 848

此外,`spring-test`模块还提供了多种功能来执行测试中通常需要的不同操作,即:

W
wizardforcel 已提交
849
*   `org.springframework.mock.web`包包含一组 Servlet API 模拟对象,用于测试 Web 上下文。例如,对象`MockMvc`允许执行 HTTP 请求(`POST``GET``PUT``DELETE`等)并验证响应(状态码、内容类型或响应主体)。
W
wizardforcel 已提交
850 851
*   `org.springframework.mock.jndi`包包含**Java 命名和目录接口****JNDI**)SPI 的实现,可用于建立简单的 JNDI 测试环境。例如,使用`SimpleNamingContextBuilder`类,我们可以在测试中使用 JNDI 数据源。
*   `org.springframework.test.jdbc`包包含类`JdbcTestUtils`,它是 JDBC 实用程序函数的集合,旨在简化标准数据库访问。
W
wizardforcel 已提交
852
*   `org.springframework.test.util`包包含类`ReflectionTestUtils`,它是一组实用方法的集合,用于在测试应用程序代码时设置非公共字段或调用私有/受保护的设置器方法。
W
init  
wizardforcel 已提交
853

W
wizardforcel 已提交
854
# 测试 Spring 启动应用程序
W
init  
wizardforcel 已提交
855

W
wizardforcel 已提交
856
如前所述,Spring Boot 是 Spring 产品组合的一个项目,旨在简化 Spring 应用程序的开发。使用 Spring Boot 的主要好处总结如下:
W
init  
wizardforcel 已提交
857

W
wizardforcel 已提交
858
*   SpringBoot应用程序只是一个 Spring`ApplicationContext`,其中使用了配置上的主要约定。多亏了这一点,我们可以更快地开始 Spring 开发。
W
wizardforcel 已提交
859 860
*   注释`@SpringBootApplication`用于标识 Spring Boot 项目中的主类。
*   提供了一系列开箱即用的非功能特性:嵌入式 servlet 容器(Tomcat、Jetty 和 Undertow)、安全性、度量、运行状况检查或外部化配置。
W
wizardforcel 已提交
861
*   创建仅使用命令`java -jar`运行的独立运行应用程序(即使对于 Web 应用程序也是如此)。
W
wizardforcel 已提交
862
*   Spring Boot**命令行界面****CLI**)允许运行 Groovy 脚本,以便使用 Spring 快速原型化。
W
wizardforcel 已提交
863
*   Spring Boot 的工作方式与任何标准 Java 库相同,也就是说,要使用它,我们只需在项目类路径中添加适当的`spring-boot-*.jar`(通常使用 Maven 或 Gradle 等构建工具)。Spring Boot 提供了许多*启动器*,旨在简化将不同库添加到类路径的过程。下表包含其中几个启动器:
W
init  
wizardforcel 已提交
864 865

| **名称** | **说明** |
W
wizardforcel 已提交
866
| --- | --- |
W
init  
wizardforcel 已提交
867
| `spring-boot-starter` | 核心启动器,包括自动配置支持和日志记录 |
W
wizardforcel 已提交
868
| `spring-boot-starter-batch` | Spring批用起动器 |
W
wizardforcel 已提交
869 870 871 872
| `spring-boot-starter-cloud-connectors` | 使用 SpringCloudConnectors 的初学者,它简化了与云平台(如 CloudFoundry 和 Heroku)中的服务的连接 |
| `spring-boot-starter-data-jpa` | 将 Spring 数据 JPA 与 Hibernate 结合使用的启动程序 |
| `spring-boot-starter-integration` | 使用 Spring 集成的启动器 |
| `spring-boot-starter-jdbc` | 将 JDBC 与 Tomcat JDBC 连接池一起使用的初学者 |
W
wizardforcel 已提交
873
| `spring-boot-starter-test` | 用于使用库(包括 JUnit、Hamcrest 和 Mockito)测试 SpringBoot应用程序的初学者 |
W
wizardforcel 已提交
874 875
| `spring-boot-starter-thymeleaf` | 用于使用 IELAF 视图构建 MVC Web 应用程序的初学者 |
| `spring-boot-starter-web` | 使用 SpringMVC 构建 Web(包括 REST)应用程序的初学者。使用 Tomcat 作为默认的嵌入式容器 |
W
wizardforcel 已提交
876
| `spring-boot-starter-websocket` | 使用 Spring 框架的 WebSocket 支持构建 WebSocket 应用程序的初学者 |
W
init  
wizardforcel 已提交
877

W
wizardforcel 已提交
878
有关 Spring Boot 的完整信息,请访问[官方参考资料](https://projects.spring.io/spring-boot/)
W
init  
wizardforcel 已提交
879

W
wizardforcel 已提交
880
SpringBoot 提供了不同的功能来简化测试。例如,它提供了`@SpringBootTest`注释,用于测试类的类级别。此注释将为这些测试创建`ApplicationContext`(与`@ContextConfiguration`类似,但用于基于 SpringBoot的应用程序)。正如我们在前面的章节中所看到的,`spring-test`模块中,我们使用注释`@ContextConfiguration(classes=… )`来指定要加载的 Bean 定义(Spring`@Configuration`。在测试 SpringBoot应用程序时,这通常不是必需的。SpringBoot 的测试注释将自动搜索主配置(如果没有明确定义)。搜索算法从包含测试的包开始,直到找到一个`@SpringBootApplication`注释类。
W
init  
wizardforcel 已提交
881

W
wizardforcel 已提交
882
SpringBoot 还促进了对 Spring 组件使用模拟。为此,提供了注释`@MockBean`。此注释允许在`ApplicationContext`中为 Bean 定义 Mockito mock。它可以是新的 Bean,但也可以替换单个现有的 Bean 定义。模拟 Bean 在每个测试方法之后自动重置。这种方法通常被称为容器内测试,与容器外测试相对应,其中使用模拟库(例如,Mockito)对Spring组件进行单元测试,以隔离,而不需要Spring`ApplicationContext`。例如,Spring 应用程序的两种单元测试的示例将在下一节中显示。
W
init  
wizardforcel 已提交
883

W
wizardforcel 已提交
884
# 用于 Spring 的 JUnit5 扩展
W
init  
wizardforcel 已提交
885

W
wizardforcel 已提交
886
为了将`spring-test`功能集成到 JUnit5 的 Jupiter 编程模型中,开发了`SpringExtension`。从Spring 5 开始,此延长件是`spring-test`模块的一部分。让我们一起来看看 JUnit5 和 Spring5 的几个示例。
W
init  
wizardforcel 已提交
887

W
wizardforcel 已提交
888
假设我们要对前一节中描述的 Spring 应用程序进行容器内集成测试,该测试由三个类组成:`MySpringApplication``MessageComponent``MessageService`。正如我们所了解的,为了针对该应用程序实施 Jupiter 测试,我们需要执行以下步骤:
W
init  
wizardforcel 已提交
889 890

1.`@ContextConfiguration`注释我们的测试类,以指定需要加载的`ApplicationContext`
W
wizardforcel 已提交
891 892
2.`@ExtendWith(SpringExtension.class)`注释我们的测试类,以启用`spring-test`进入 Jupiter。
3.  注入我们要在测试类中评估的 Spring 组件。
W
init  
wizardforcel 已提交
893 894 895 896
4.  实施我们的测试(`@Test`

例如:

W
wizardforcel 已提交
897
```java
W
init  
wizardforcel 已提交
898 899
package io.github.bonigarcia;

W
wizardforcel 已提交
900
import static org.junit.jupiter.api.Assertions.assertEquals;
W
init  
wizardforcel 已提交
901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { MySpringApplication.class })
class SimpleSpringTest {

    @Autowired
    public MessageComponent messageComponent;

    @Test
    public void test() {
W
wizardforcel 已提交
917
        assertEquals("Hello world!", messageComponent.getMessage());
W
init  
wizardforcel 已提交
918 919 920 921 922
    }

}
```

W
wizardforcel 已提交
923
这是一个非常简单的示例,其中评估了名为`MessageComponent`的Spring组件。当本测试开始时,我们的`ApplicationContext`启动,所有Spring组件都在里面。之后,在本例中,将 Bean`MessageComponent`注入测试中,只需调用方法`getMessage()`并验证其响应即可对其进行评估。
W
init  
wizardforcel 已提交
924

W
wizardforcel 已提交
925
值得回顾一下此测试需要哪些依赖项。使用 Maven 时,这些依赖项如下所示:
W
init  
wizardforcel 已提交
926

W
wizardforcel 已提交
927
```java
W
init  
wizardforcel 已提交
928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
```

W
wizardforcel 已提交
949
另一方面,如果我们使用 Gradle,dependencies 子句将如下所示:
W
init  
wizardforcel 已提交
950

W
wizardforcel 已提交
951
```java
W
init  
wizardforcel 已提交
952 953 954 955 956 957 958 959
dependencies {
    compile("org.springframework:spring-context:${springVersion}")
    testCompile("org.springframework:spring-test:${springVersion}")
    testCompile("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
    testRuntime("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
}
```

W
wizardforcel 已提交
960
请注意,在这两种情况下,都需要`spring-context`依赖项来实现应用程序,然后我们需要`spring-test``junit-jupiter`来测试它。为了实现等效的应用程序和测试,但这次使用 Spring Boot,首先我们需要更改我们的`pom.xml`(使用 Maven 时):
W
init  
wizardforcel 已提交
961

W
wizardforcel 已提交
962
```java
W
init  
wizardforcel 已提交
963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
<project  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>junit5-spring-boot</artifactId>
    <version>1.0.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.M3</version>
    </parent>

    <properties>
        <junit.jupiter.version>5.0.0</junit.jupiter.version>
        <junit.platform.version>1.0.0</junit.platform.version>
        <java.version>1.8</java.version>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.junit.platform</groupId>
                        <artifactId>junit-platform-surefire-provider</artifactId>
                        <version>${junit.platform.version}</version>
                    </dependency>
                    <dependency>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter-engine</artifactId>
                        <version>${junit.jupiter.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <url>https://repo.spring.io/libs-milestone</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <url>https://repo.spring.io/milestone</url>
        </pluginRepository>
    </pluginRepositories>

</project>
```

W
wizardforcel 已提交
1052
或我们的`build.gradle`(使用 Gradle 时):
W
init  
wizardforcel 已提交
1053

W
wizardforcel 已提交
1054
```java
W
init  
wizardforcel 已提交
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
buildscript {
    ext {
        springBootVersion = '2.0.0.M3'
        junitPlatformVersion = '1.0.0'
    }

    repositories {
        mavenCentral()
        maven {
            url 'https://repo.spring.io/milestone'
        }
    }

    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.junit.platform:junit-platform-gradle-plugin:${junitPlatformVersion}")
    }
}

repositories {
    mavenCentral()
    maven {
        url 'https://repo.spring.io/libs-milestone'
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.junit.platform.gradle.plugin'

jar {
    baseName = 'junit5-spring-boot'
    version = '1.0.0'
}

compileTestJava {
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    options.compilerArgs += '-parameters'
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter')
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
    testRuntime("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
}
```

W
wizardforcel 已提交
1107
为了将原始 Spring 应用程序转换为 Spring Boot,我们的组件(在示例中称为`MessageComponent``MessageService`)将完全相同,但我们的主类将发生一些变化(参见此处)。请注意,我们在类级别使用注释`@SpringBootApplication`,使用 Spring Boot 的典型引导机制实现 main 方法。出于日志记录的目的,我们正在实现一个用`@PostConstruct`注释的方法。此方法将在应用程序上下文启动之前触发:
W
init  
wizardforcel 已提交
1108

W
wizardforcel 已提交
1109
```java
W
init  
wizardforcel 已提交
1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
package io.github.bonigarcia;

import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MySpringBootApplication {
W
wizardforcel 已提交
1121
    final Logger log = LoggerFactory.getLogger(MySpringBootApplication.class);
W
init  
wizardforcel 已提交
1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137

    @Autowired
    public MessageComponent messageComponent;

    @PostConstruct
    private void setup() {
        log.info("*** {} ***", messageComponent.getMessage());
    }

    public static void main(String[] args) throws Exception {
        new SpringApplication(MySpringBootApplication.class).run(args);
    }

}
```

W
wizardforcel 已提交
1138
测试的实施将非常简单。我们需要做的唯一更改是用`@SpringBootTest`而不是`@ContextConfiguration`注释测试(Spring Boot 自动查找并启动我们的`ApplicationContext`
W
init  
wizardforcel 已提交
1139

W
wizardforcel 已提交
1140
```java
W
init  
wizardforcel 已提交
1141 1142
package io.github.bonigarcia;

W
wizardforcel 已提交
1143
import static org.junit.jupiter.api.Assertions.assertEquals;
W
init  
wizardforcel 已提交
1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
 import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@SpringBootTest
class SimpleSpringBootTest {

    @Autowired
    public MessageComponent messagePrinter;

    @Test
    public void test() {
W
wizardforcel 已提交
1159
        assertEquals("Hello world!", messagePrinter.getMessage());
W
init  
wizardforcel 已提交
1160 1161 1162 1163 1164
    }

}
```

W
wizardforcel 已提交
1165
在控制台中执行测试时,我们可以看到应用程序实际上是在测试之前启动的(请注意开头的 spring ASCII 横幅)。
W
init  
wizardforcel 已提交
1166

W
wizardforcel 已提交
1167
之后,我们的测试使用`ApplicationContext`对一个Spring组件进行验证,结果测试成功:
W
init  
wizardforcel 已提交
1168 1169 1170

![](img/00115.gif)

W
wizardforcel 已提交
1171
使用 springboot 执行测试
W
init  
wizardforcel 已提交
1172

W
wizardforcel 已提交
1173
最后,我们将看到一个使用 Spring Boot 实现的简单 Web 应用程序。关于依赖项,我们需要做的唯一更改是包含已启动的`spring-boot-starter-web`(而不是通用的`spring-boot-starter`)。就这样,我们可以开始实现基于 Spring 的 Web 应用程序了。
W
init  
wizardforcel 已提交
1174

W
wizardforcel 已提交
1175
我们将实现一个非常简单的`@Controller`,即 Springbean,它处理来自浏览器的请求。在我们的示例中,控制器映射的唯一 URL 是默认资源`/`
W
init  
wizardforcel 已提交
1176

W
wizardforcel 已提交
1177
```java
W
init  
wizardforcel 已提交
1178 1179
package io.github.bonigarcia;

W
wizardforcel 已提交
1180
import static org.springframework.web.bind.annotation.RequestMethod.GET;
W
init  
wizardforcel 已提交
1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class WebController {

    @Autowired
    private PageService pageService;

W
wizardforcel 已提交
1192
    @RequestMapping(value = "/", method = GET)
W
init  
wizardforcel 已提交
1193 1194 1195 1196 1197 1198 1199 1200 1201
    public String greeting() {
        return pageService.getPage();
    }

}
```

该组件注入一个名为`PageService`的服务,负责将响应请求而要加载的实际页面返回给`/`。这项服务的内容也非常简单:

W
wizardforcel 已提交
1202
```java
W
init  
wizardforcel 已提交
1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216
package io.github.bonigarcia;

import org.springframework.stereotype.Service;

@Service
public class PageService {

    public String getPage() {
        return "/index.html";
    }

}
```

W
wizardforcel 已提交
1217
按照惯例(我们在这里使用 SpringBoot),基于 Spring 的 Web 应用程序的静态资源位于项目类路径中名为`static`的文件夹中。按照 Maven/Gradle 项目的结构,该文件夹位于`src/main/resources`路径中(见下面的屏幕截图)。请注意,这里有两个页面(我们在测试中从一个页面切换到另一个页面,请继续关注):
W
init  
wizardforcel 已提交
1218 1219 1220

![](img/00116.jpeg)

W
wizardforcel 已提交
1221
示例项目`junit5 spring boot web`的内容
W
init  
wizardforcel 已提交
1222

W
wizardforcel 已提交
1223
让我们继续讨论有趣的部分:测试。我们正在这个项目中实施三个 Jupiter 测试。第一个用于验证对页面`/index.html`的直接调用。如前所述,该测试需要使用Spring延长件(`@ExtendWith(SpringExtension.class)`并声明为Spring启动测试(`@SpringBootTest`。为了实现对 Web 应用程序的请求,我们使用了一个`MockMvc`实例,通过几种方式(HTTP 响应代码、内容类型和响应内容体)验证响应。此实例使用 Spring Boot 注释`@AutoConfigureMockMvc`自动配置。
W
init  
wizardforcel 已提交
1224

W
wizardforcel 已提交
1225
在 Spring Boot 之外,可以使用名为`MockMvcBuilders`的构建器类来创建对象`MockMvc`,而不是使用`@AutoConfigureMockMvc`。在本例中,应用程序上下文用作该生成器的参数。
W
init  
wizardforcel 已提交
1226

W
wizardforcel 已提交
1227
```java
W
init  
wizardforcel 已提交
1228 1229
package io.github.bonigarcia;

W
wizardforcel 已提交
1230 1231 1232 1233
import static org.hamcrest.core.StringContains.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
W
init  
wizardforcel 已提交
1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class IndexTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    void testIndex() throws Exception {
W
wizardforcel 已提交
1253 1254 1255
        mockMvc.perform(get("/index.html")).andExpect(status().isOk())
                .andExpect(content().contentType("text/html")).andExpect(
                        content().string(containsString("This is index 
W
init  
wizardforcel 已提交
1256 1257 1258 1259 1260 1261
                        page")));
    }

}
```

W
wizardforcel 已提交
1262
同样,在 Shell 中运行此测试,我们检查应用程序是否实际执行。默认情况下,嵌入式 Tomcat 监听端口`8080`。之后,测试成功执行:
W
init  
wizardforcel 已提交
1263 1264 1265 1266 1267

![](img/00117.gif)

容器内首次测试的控制台输出

W
wizardforcel 已提交
1268
第二个测试类似,但作为一个差异因素,它使用测试能力`@MockBean`通过模拟覆盖Spring组件(在本例中为`PageService`)。在测试主体中,首先我们将模拟的方法`getPage`插桩,以将组件的默认响应更改为`redirect:/page.html`。因此,当使用对象`MockMvc`在测试中请求资源`/`时,我们将获得一个 HTTP 302 响应(重定向)到资源`/page.html`(实际上是一个现有页面,如项目截图所示):
W
init  
wizardforcel 已提交
1269

W
wizardforcel 已提交
1270
```java
W
init  
wizardforcel 已提交
1271 1272
package io.github.bonigarcia;

W
wizardforcel 已提交
1273 1274 1275 1276
import static org.mockito.Mockito.doReturn;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
W
init  
wizardforcel 已提交
1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class RedirectTest {

    @MockBean
    PageService pageService;

    @Autowired
    MockMvc mockMvc;

    @Test
    void test() throws Exception {
W
wizardforcel 已提交
1300 1301 1302
        doReturn("redirect:/page.html").when(pageService).getPage();
        mockMvc.perform(get("/")).andExpect(status().isFound())
                .andExpect(redirectedUrl("/page.html"));
W
init  
wizardforcel 已提交
1303 1304 1305 1306 1307
    }

}
```

W
wizardforcel 已提交
1308
类似地,在 Shell 中,我们可以确认测试启动了 Spring 应用程序,然后正确执行:
W
init  
wizardforcel 已提交
1309 1310 1311 1312 1313

![](img/00118.gif)

容器内第二次测试的控制台输出

W
wizardforcel 已提交
1314
本项目的最后一个测试是*集装箱外*测试的一个示例。在前面的测试示例中,测试中使用了 Spring 上下文。另一方面,以下内容完全依赖于 Mockito 来执行系统的组件,这次没有启动 Spring 应用程序上下文。注意,我们在这里使用的是`MockitoExtension`扩展,使用组件`WebController`作为我们的 SUT(`@InjectMocks`),组件`PageService`作为 DOC(`@Mock`
W
init  
wizardforcel 已提交
1315

W
wizardforcel 已提交
1316
```java
W
init  
wizardforcel 已提交
1317 1318
package io.github.bonigarcia;

W
wizardforcel 已提交
1319 1320 1321 1322
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
W
init  
wizardforcel 已提交
1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import io.github.bonigarcia.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class OutOfContainerTest {

    @InjectMocks
    private WebController webController;

    @Mock
    private PageService pageService;

    @Test
    void test() {
W
wizardforcel 已提交
1341 1342 1343
        when(pageService.getPage()).thenReturn("/my-page.html");
        assertEquals("/my-page.html", webController.greeting());
        verify(pageService, times(1)).getPage();
W
init  
wizardforcel 已提交
1344 1345 1346 1347 1348
    }

}
```

W
wizardforcel 已提交
1349
这一次,在执行测试时,我们没有看到 spring 跟踪,因为在执行测试之前应用程序容器没有启动:
W
init  
wizardforcel 已提交
1350 1351 1352 1353 1354

![](img/00119.gif)

容器外测试的控制台输出

W
wizardforcel 已提交
1355
# Selenium
W
init  
wizardforcel 已提交
1356

W
wizardforcel 已提交
1357
[Selenium](http://www.seleniumhq.org/) 是一个开源的 Web 测试框架,自 2008 年成立以来,已将自己建立为*事实上的* Web 自动化库。在下一节中,我们将回顾 Selenium 的主要特性以及如何在 JUnit5 测试中使用它。
W
init  
wizardforcel 已提交
1358

W
wizardforcel 已提交
1359
# 简言之,Selenium
W
init  
wizardforcel 已提交
1360

W
wizardforcel 已提交
1361
Selenium由不同的项目组成。首先,我们找到了Selenium IDE。它是一个 Firefox 插件,为 Web 应用程序实现**记录和回放****R&P**)模式。因此,它允许记录与 Firefox 的手动交互以及以自动方式进行的回放。
W
init  
wizardforcel 已提交
1362

W
wizardforcel 已提交
1363
第二个项目名为**SeleniumRemoteControl****RC**)。该组件能够使用不同的编程语言(如 Java、C#、Python、Ruby、PHP、Perl 或 JavaScript)自动驱动不同类型的浏览器。该组件在 SUT 中注入了一个 JavaScript 库(称为 Selenium Core)。该库由一个名为 Selenium RC Server 的中间组件控制,该组件接收来自测试代码的请求(参见下图)。由于同源策略,Selenium RC 存在重要的安全问题。
W
init  
wizardforcel 已提交
1364

W
wizardforcel 已提交
1365
出于这个原因,2016 年它被弃用,取而代之的是 Selenium WebDriver:
W
init  
wizardforcel 已提交
1366 1367 1368

![](img/00120.jpeg)

W
wizardforcel 已提交
1369
Selenium RC 模式
W
init  
wizardforcel 已提交
1370

W
wizardforcel 已提交
1371
我们回顾 SeleniumRC 只是为了介绍 SeleniumWebDriver。如今,Selenium RC 已被弃用,它的使用也极不受欢迎。
W
init  
wizardforcel 已提交
1372

W
wizardforcel 已提交
1373
从功能的角度来看,SeleniumWebDriver 相当于 RC(也就是说,允许使用代码控制浏览器)。作为一个不同的方面,Selenium WebDriver 使用每个浏览器对自动化的本机支持来调用浏览器。Selenium WebDriver 提供的语言绑定(在下图中标记为 Test)与特定于浏览器的二进制文件进行通信,该二进制文件充当真实浏览器之间的桥梁。例如,这个二进制文件被称为[ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/) 用于 Chrome,[GeckoDriver](https://github.com/mozilla/geckodriver) 用于 Firefox。测试和驱动程序之间的通信是使用 JSON 消息通过 HTTP 通过所谓的 JSON Wire 协议完成的。
W
init  
wizardforcel 已提交
1374

W
wizardforcel 已提交
1375
此机制最初由 WebDriver 团队提出,并在 [W3C WebDriver API](https://www.w3.org/TR/webdriver/) 中进行了标准化:
W
init  
wizardforcel 已提交
1376 1377 1378 1379 1380

![](img/00121.jpeg)

Selenium WebDriver schema

W
wizardforcel 已提交
1381
Selenium 项目组合的最后一个项目称为 Selenium Grid。它可以看作是 SeleniumWebDriver 的扩展,因为它允许在远程机器上分发浏览器执行。有许多节点,每个节点运行在不同的操作系统和不同的浏览器上。集线器服务器跟踪节点并向其发送代理请求(参见下图):
W
init  
wizardforcel 已提交
1382 1383 1384 1385 1386

![](img/00122.jpeg)

Selenium Grid schema

W
wizardforcel 已提交
1387
下表总结了 WebDriver API 的主要功能:
W
init  
wizardforcel 已提交
1388

W
wizardforcel 已提交
1389
WebDriver 对象创建:它允许创建 WebDriver 实例,这些实例从测试代码中用于远程控制浏览器。
W
init  
wizardforcel 已提交
1390

W
wizardforcel 已提交
1391
```java
W
init  
wizardforcel 已提交
1392 1393 1394 1395 1396 1397 1398
WebDriver driver = new FirefoxDriver();

WebDriver driver = new ChromeDriver();

WebDriver driver = new OperaDriver();
```

W
wizardforcel 已提交
1399
导航:它允许导航到给定的 URL。
W
init  
wizardforcel 已提交
1400

W
wizardforcel 已提交
1401
```java
W
init  
wizardforcel 已提交
1402 1403 1404
driver.get("http://junit.org/junit5/");
```

W
wizardforcel 已提交
1405
定位元素:它允许使用不同的策略识别网页(WebElement)中的元素:按 id、名称、类名、CSS 选择器、链接文本、标记名或 XPath 
W
init  
wizardforcel 已提交
1406

W
wizardforcel 已提交
1407
```java
W
wizardforcel 已提交
1408 1409 1410 1411 1412 1413 1414
WebElement webElement = driver.findElement(By.id("id"));
driver.findElement(By.name("name"));
driver.findElement(By.className("class"));
driver.findElement(By.cssSelector("cssInput"));
driver.findElement(By.linkText("text"));
driver.findElement(By.tagName("tag name"));
driver.findElement(By.xpath("/html/body/div[4]"));
W
init  
wizardforcel 已提交
1415 1416
```

W
wizardforcel 已提交
1417
与元素交互:从给定的 WebElement,我们可以执行不同类型的自动交互,例如单击元素、键入文本或清除输入字段、读取属性等。
W
init  
wizardforcel 已提交
1418

W
wizardforcel 已提交
1419
```java
W
init  
wizardforcel 已提交
1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431
webElement.click();
webElement.sendKeys("text");
webElement.clear();
String text = webElement.getText();
String href = webElement.getAttribute("href");
String css = webElement.getCssValue("css");
Dimension dim = webElement.getSize();
boolean enabled = webElement.isEnabled();
boolean selected = webElement.isSelected();
boolean displayed = webElement.isDisplayed();
```

W
wizardforcel 已提交
1432
句柄等待:WebDriver 可以显式和隐式地处理等待。
W
init  
wizardforcel 已提交
1433

W
wizardforcel 已提交
1434
```java
W
init  
wizardforcel 已提交
1435 1436 1437 1438 1439
// Explicit
WebDriverWait wait = new WebDriverWait(driver, 30);
wait.until(ExpectedConditions);

// Implicit wait
W
wizardforcel 已提交
1440
driver.manage().timeouts().implicitlyWait(30, SECONDS);
W
init  
wizardforcel 已提交
1441 1442
```

W
wizardforcel 已提交
1443
XPath(XML 路径语言)是一种用于构建表达式以解析和处理类似 XML 的文档(例如 HTML)的语言
W
init  
wizardforcel 已提交
1444

W
wizardforcel 已提交
1445
# 用于 Selenium 的 JUnit5 扩展
W
init  
wizardforcel 已提交
1446

W
wizardforcel 已提交
1447
为了简化 SeleniumWebDriver 在 JUnit5 中的使用,可以使用名为`selenium-jupiter`的开源 JUnit5 扩展。这个扩展是使用 JUnit5 的扩展模型提供的依赖注入功能构建的。由于这个特性,不同类型的对象可以作为参数注入 JUnit5 中的`@Test`方法中。具体来说,`selenium-jupiter`允许注入`WebDriver`接口的子类型(例如,`ChromeDriver``FirefoxDriver`等)。
W
init  
wizardforcel 已提交
1448

W
wizardforcel 已提交
1449
使用`selenium-jupiter`非常简单。首先,我们需要在项目中导入依赖项(通常作为测试依赖项)。在 Maven 中,操作如下:
W
init  
wizardforcel 已提交
1450

W
wizardforcel 已提交
1451
```java
W
init  
wizardforcel 已提交
1452 1453 1454 1455 1456 1457 1458 1459 1460 1461
<dependency>
        <groupId>io.github.bonigarcia</groupId>
        <artifactId>selenium-jupiter</artifactId>
        <version>${selenium-jupiter.version}</version>
        <scope>test</scope>
</dependency>
```

`selenium-jupiter`依赖于几个库,这些库在我们的项目中作为传递性`dependencies`添加,即:

W
wizardforcel 已提交
1462
*   `Selenium-java``org.seleniumhq.selenium:selenium-java`:Selenium WebDriver 的 Java 库。
W
wizardforcel 已提交
1463
*   `WebDriverManager``io.github.bonigarcia:webdrivermanager`):用于在 Java 运行时自动管理 [Selenium WebDriver 二进制文件的 Java 库](https://github.com/bonigarcia/webdrivermanager)
W
wizardforcel 已提交
1464
*   Appium(`io.appium:java-client`):Appium 的 Java 客户端,测试框架,扩展 Selenium [以自动测试本地、混合和移动 Web 应用程序](http://appium.io/)
W
init  
wizardforcel 已提交
1465

W
wizardforcel 已提交
1466
一旦`selenium-jupiter`包含在我们的项目中,我们需要在 JUnit5 测试中声明`selenium-jupiter`扩展,只需用`@ExtendWith(SeleniumExtension.class)`注释它。然后,我们需要在`@Test`方法中包含一个或多个参数,其类型实现 WebDriver 接口,并且`selenium-jupiter`在内部控制 WebDriver 对象的生命周期。`selenium-jupiter`支持的 WebDriver 子类型如下:
W
init  
wizardforcel 已提交
1467

W
wizardforcel 已提交
1468 1469 1470 1471 1472 1473 1474 1475
*   `ChromeDriver`:用于控制 Google Chrome 浏览器。
*   `FirefoxDriver`:用于控制 Firefox 浏览器。
*   `EdgeDriver`:用于控制 Microsoft Edge 浏览器。
*   `OperaDriver`:用于控制 Opera 浏览器。
*   `SafariDriver`:用于控制 Apple Safari 浏览器(仅适用于 OSX El Capitan 或更高版本)。
*   `HtmlUnitDriver`:用于控制 HtmlUnit(无头浏览器,即没有 GUI 的浏览器)。
*   `PhantomJSDriver`:用于控制 PhantomJS(另一种无头浏览器)。
*   `InternetExplorerDriver`:用于控制 Microsoft Internet Explorer。虽然支持此浏览器,但不推荐使用 Internet Explorer(支持 Edge),并且强烈建议不要使用它。
W
init  
wizardforcel 已提交
1476
*   `RemoteWebDriver`:用于控制远程浏览器(Selenium Grid)。
W
wizardforcel 已提交
1477
*   `AppiumDriver`:用于控制移动设备(Android 和 iOS)。
W
init  
wizardforcel 已提交
1478

W
wizardforcel 已提交
1479
考虑下面的类,它使用了胡 T0T,即使用 MultT1。本例定义了三个测试,它们将使用本地浏览器执行。第一个(名为`testWithChrome`)使用 Chrome 作为浏览器。为此,由于`selenium-jupiter`的依赖注入特性,方法只需使用`ChromeDriver`类型声明一个方法参数。然后,在测试主体中,在该对象中调用`WebDriver`API。请注意,此简单测试将打开一个网页,并断言标题与预期一致。接下来,test(`testWithFirefoxAndOpera`类似,但这次同时使用两种不同的浏览器:Firefox(使用`FirefoxDriver`实例)和 Opera(使用`OperaDriver`实例)。第三个也是最后一个测试(`testWithHeadlessBrowsers`声明并使用两个无头浏览器(`HtmlUnit``PhantomJS`):
W
init  
wizardforcel 已提交
1480

W
wizardforcel 已提交
1481
```java
W
init  
wizardforcel 已提交
1482 1483
package io.github.bonigarcia;

W
wizardforcel 已提交
1484 1485
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
W
init  
wizardforcel 已提交
1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499
 import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import org.openqa.selenium.opera.OperaDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;

@ExtendWith(SeleniumExtension.class)
public class LocalWebDriverTest {

    @Test
    public void testWithChrome(ChromeDriver chrome) {
        chrome.get("https://bonigarcia.github.io/selenium-jupiter/");
W
wizardforcel 已提交
1500
        assertTrue(chrome.getTitle().startsWith("selenium-jupiter"));
W
init  
wizardforcel 已提交
1501 1502 1503 1504 1505 1506 1507
    }

    @Test
    public void testWithFirefoxAndOpera(FirefoxDriver firefox,
            OperaDriver opera) {
        firefox.get("http://www.seleniumhq.org/");
        opera.get("http://junit.org/junit5/");
W
wizardforcel 已提交
1508 1509
        assertTrue(firefox.getTitle().startsWith("Selenium"));
        assertTrue(opera.getTitle().equals("JUnit 5"));
W
init  
wizardforcel 已提交
1510 1511 1512 1513 1514 1515 1516
    }

    @Test
    public void testWithHeadlessBrowsers(HtmlUnitDriver htmlUnit,
            PhantomJSDriver phantomjs) {
        htmlUnit.get("https://bonigarcia.github.io/selenium-jupiter/");
        phantomjs.get("https://bonigarcia.github.io/selenium-jupiter/");
W
wizardforcel 已提交
1517 1518
        assertTrue(htmlUnit.getTitle().contains("JUnit 5 extension"));
        assertNotNull(phantomjs.getPageSource());
W
init  
wizardforcel 已提交
1519 1520 1521 1522 1523
    }

}
```

W
wizardforcel 已提交
1524
为了正确执行这个测试类,应该在运行它之前安装所需的浏览器(Chrome、Firefox 和 Opera)。另一方面,无头浏览器(HtmlUnit 和 PhantomJS)作为 Java 依赖项使用,因此不需要手动安装它们。
W
init  
wizardforcel 已提交
1525

W
wizardforcel 已提交
1526
让我们看另一个例子,这次使用远程浏览器(即 Selenium 网格)。同样,这个类使用了`selenium-jupiter`扩展名。测试(`testWithRemoteChrome`声明了一个名为`remoteChrome`的参数,类型为`RemoteWedbrider`。此参数用`@DriverUrl``@DriverCapabilities`注释,分别指定 Selenium 服务器(或集线器)URL 和所需的功能。关于功能,我们正在配置使用 Chrome 浏览器版本 59:
W
init  
wizardforcel 已提交
1527

W
wizardforcel 已提交
1528
要正确运行此测试,应该在本地主机中启动并运行 Selenium 服务器,并且需要在集线器中注册节点(Chrome 59)。
W
init  
wizardforcel 已提交
1529

W
wizardforcel 已提交
1530
```java
W
init  
wizardforcel 已提交
1531 1532
package io.github.bonigarcia;

W
wizardforcel 已提交
1533
import static org.junit.jupiter.api.Assertions.assertTrue;
W
init  
wizardforcel 已提交
1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550
 import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.remote.RemoteWebDriver;

@ExtendWith(SeleniumExtension.class)
public class RemoteWebDriverTest {

    @Test
    void testWithRemoteChrome(
            @DriverUrl("http://localhost:4444/wd/hub") 
            @DriverCapabilities(capability = {
                   @Capability(name = "browserName", value ="chrome"),
                   @Capability(name = "version", value = "59") }) 
                   RemoteWebDriver remoteChrome)
            throws InterruptedException {
        remoteChrome.get("https://bonigarcia.github.io/selenium-    
            jupiter/");
W
wizardforcel 已提交
1551
        assertTrue(remoteChrome.getTitle().contains("JUnit 5 
W
init  
wizardforcel 已提交
1552 1553 1554 1555 1556 1557
            extension"));
    }

}
```

W
wizardforcel 已提交
1558
在本节的最后一个示例中,我们使用了`AppiumDriver`。具体来说,我们将在 Android 模拟设备(`@DriverCapabilities`中)中使用 Chrome 浏览器设置为功能。同样,此模拟器需要在运行测试的机器上启动并运行:
W
init  
wizardforcel 已提交
1559

W
wizardforcel 已提交
1560
```java
W
init  
wizardforcel 已提交
1561 1562
package io.github.bonigarcia;

W
wizardforcel 已提交
1563
import static org.junit.jupiter.api.Assertions.assertTrue;
W
init  
wizardforcel 已提交
1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584
 import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;
import io.appium.java_client.AppiumDriver;

@ExtendWith(SeleniumExtension.class)
public class AppiumTest {

    @DriverCapabilities
    DesiredCapabilities capabilities = new DesiredCapabilities();
    {
        capabilities.setCapability("browserName", "chrome");
        capabilities.setCapability("deviceName", "Android");
    }

    @Test
    void testWithAndroid(AppiumDriver<WebElement> android) {
        String context = android.getContext();
        android.context("NATIVE_APP");
W
wizardforcel 已提交
1585
        android.findElement(By.id("com.android.chrome:id/terms_accept"))
W
init  
wizardforcel 已提交
1586
                .click();
W
wizardforcel 已提交
1587
        android.findElement(By.id("com.android.chrome:id/negative_button"))
W
init  
wizardforcel 已提交
1588 1589 1590
                .click();
        android.context(context);
        android.get("https://bonigarcia.github.io/selenium-jupiter/");
W
wizardforcel 已提交
1591
        assertTrue(android.getTitle().contains("JUnit 5 extension"));
W
init  
wizardforcel 已提交
1592 1593 1594 1595 1596
    }

}
```

W
wizardforcel 已提交
1597
有关`selenium-jupiter`的更多示例,请访问[这里](https://bonigarcia.github.io/selenium-jupiter/)
W
init  
wizardforcel 已提交
1598

W
wizardforcel 已提交
1599
# Cucumber
W
init  
wizardforcel 已提交
1600

W
wizardforcel 已提交
1601
[Cucumber](https://cucumber.io/) 是一个测试框架,旨在自动化按照**行为驱动开发****BDD**)风格编写的验收测试。Cucumber 是用 Ruby 编写的,不过也有其他语言(包括 Java、JavaScript 和 Python)的实现。
W
init  
wizardforcel 已提交
1602

W
wizardforcel 已提交
1603
# Cucumber壳
W
init  
wizardforcel 已提交
1604

W
wizardforcel 已提交
1605
Cucumber 执行用名为 Gherkin 的语言编写的指定测试。它是一种具有给定结构的纯文本自然语言(例如,英语或 Cucumber 支持的其他 60 多种语言之一)。Cucumber被设计成供非程序员使用,通常是客户、业务分析、经理等等。
W
init  
wizardforcel 已提交
1606

W
wizardforcel 已提交
1607
Cucumber文件的扩展名为`*.feature*`
W
init  
wizardforcel 已提交
1608

W
wizardforcel 已提交
1609
在Cucumber文件中,非空行可以以关键字开头,然后是自然语言中的文本。主要关键词如下:
W
init  
wizardforcel 已提交
1610 1611 1612 1613 1614 1615 1616

*   **特性**:待测试软件特性的高层描述。它可以看作是一个用例描述。
*   **场景**:说明业务规则的具体示例。场景遵循相同的模式:
    *   描述初始上下文。
    *   描述一个事件。
    *   描述预期结果。

W
wizardforcel 已提交
1617
这些动作在Cucumber术语中被称为步骤,主要是**给出****当**,或**然后**
W
init  
wizardforcel 已提交
1618 1619 1620 1621 1622 1623

有两个额外的步骤:**和**(用于逻辑和不同的步骤)和**但**(用于**和**的否定形式)。

*   **给定**:测试开始前的先决条件和初始状态。
*   **当**时:用户在测试过程中采取的动作。
*   **然后**:在**当**条款中采取行动的结果。
W
wizardforcel 已提交
1624
*   **背景**:为了避免在不同场景中重复步骤,关键字 Background 允许声明这些步骤,这些步骤在后续场景中重复使用。
W
wizardforcel 已提交
1625
*   **场景大纲**:步骤用变量标记的场景(使用符号`<``>`
W
init  
wizardforcel 已提交
1626 1627
*   **示例**:场景大纲声明后面总是有一个或多个示例部分,这是一个容器表,其中包含**场景大纲**中声明的变量的值。

W
wizardforcel 已提交
1628
当一行不以关键字开头时,Cucumber 不会解释该行。它用于自定义描述。
W
init  
wizardforcel 已提交
1629

W
wizardforcel 已提交
1630
一旦我们定义了要测试的功能,我们就需要所谓的*步骤定义*,它允许将纯文本Cucumber翻译成实际执行 SUT 的动作。在 Java 中,可以很容易地通过注释对步骤实现的方法进行注释:`@Given``@Then``@When``@And``@But`。每个步骤的字符串值可以包含正则表达式,这些正则表达式在方法中映射为字段。请参见下一节中的示例。
W
init  
wizardforcel 已提交
1631

W
wizardforcel 已提交
1632
# Cucumber JUnit 5 的扩展
W
init  
wizardforcel 已提交
1633

W
wizardforcel 已提交
1634
Cucumber artifacts for Java 的最新版本包含一个 JUnit 5 Cucumber 扩展。本节包含在 Gherkin 和 JUnit 5 中定义的一个功能的完整示例,用于使用 Cucumber 执行该功能。与往常一样,[本例的源代码托管在 GitHub 上](https://github.com/bonigarcia/mastering-junit5)
W
init  
wizardforcel 已提交
1635 1636 1637 1638 1639

包含此示例的项目结构如下所示:

![](img/00123.jpeg)

W
wizardforcel 已提交
1640
JUnit5 与 Cucumber 项目结构和内容
W
init  
wizardforcel 已提交
1641

W
wizardforcel 已提交
1642
首先,我们需要创建Cucumber文件,该文件旨在测试一个简单的计算器系统。这个计算器将是 SUT 或我们的测试。我们的专题文件内容如下:
W
init  
wizardforcel 已提交
1643

W
wizardforcel 已提交
1644
```java
W
init  
wizardforcel 已提交
1645 1646
Feature: Basic Arithmetic
  Background: A Calculator
W
wizardforcel 已提交
1647
    Given a calculator I just turned on
W
init  
wizardforcel 已提交
1648
  Scenario: Addition
W
wizardforcel 已提交
1649 1650
    When I add 4 and 5
    Then the result is 9
W
init  
wizardforcel 已提交
1651
  Scenario: Substraction
W
wizardforcel 已提交
1652 1653
    When I substract 7 to 2
    Then the result is 5
W
init  
wizardforcel 已提交
1654
  Scenario Outline: Several additions
W
wizardforcel 已提交
1655 1656
    When I add <a> and <b>
*    Then the result is <c>
W
init  
wizardforcel 已提交
1657 1658 1659 1660 1661 1662
*  Examples: Single digits
    | a | b | c  |
    | 1 | 2 | 3  |
    | 3 | 7 | 10 |
```

W
wizardforcel 已提交
1663
然后,我们需要实现步骤定义。如前所述,我们使用注释和正则表达式将Cucumber文件中包含的文本映射到 SUT 的实际练习,具体取决于以下步骤:
W
init  
wizardforcel 已提交
1664

W
wizardforcel 已提交
1665
```java
W
init  
wizardforcel 已提交
1666 1667
package io.github.bonigarcia;

W
wizardforcel 已提交
1668
import static org.junit.jupiter.api.Assertions.assertEquals;
W
init  
wizardforcel 已提交
1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
 public class CalculatorSteps {

    private Calculator calc;

    @Given("^a calculator I just turned on$")
    public void setup() {
        calc = new Calculator();
    }

    @When("^I add (\\d+) and (\\d+)$")
    public void add(int arg1, int arg2) {
        calc.push(arg1);
        calc.push(arg2);
        calc.push("+");
    }

    @When("^I substract (\\d+) to (\\d+)$")
    public void substract(int arg1, int arg2) {
        calc.push(arg1);
        calc.push(arg2);
        calc.push("-");
    }

    @Then("^the result is (\\d+)$")
    public void the_result_is(double expected) {
W
wizardforcel 已提交
1698
        assertEquals(expected, calc.value());
W
init  
wizardforcel 已提交
1699 1700 1701 1702 1703
    }

}
```

W
wizardforcel 已提交
1704
当然,我们仍然需要实现 JUnit5 测试。为了实现 Cucumber 与 JUnit 5 的集成,Cucumber 扩展需要通过`@ExtendWith(CucumberExtension.class)`在我们班注册。在内部,`CucumberExtension`实现了 Jupiter 扩展模型的`ParameterResolver`回调。目标是将Cucumber特征的相应测试作为 Jupiter`DynamicTest`对象注入测试中。注意,在示例中,`@TestFactory`是如何使用的。
W
init  
wizardforcel 已提交
1705

W
wizardforcel 已提交
1706
或者,我们可以用`@CucumberOptions`注释我们的测试类。此注释允许为我们的测试配置 Cumber 设置。此批注允许的元素包括:
W
init  
wizardforcel 已提交
1707

W
wizardforcel 已提交
1708
*   `plugin`:内置格式化程序:pretty、progress、JSON、usage 等。默认值:`{}`
W
init  
wizardforcel 已提交
1709 1710 1711 1712 1713 1714 1715 1716
*   `dryRun`:检查所有步骤是否都有定义。默认值:`false`
*   `features`:特征文件的路径。默认值:`{}`
*   `glue`:步骤定义的路径。默认值:`{}`
*   `tags`:待执行特征中的标签。默认值`{}`
*   `monochrome`:以可读的方式显示控制台输出。默认值:`false`
*   `format`:要使用的报表格式化程序。默认值:`{}`
*   `strict`:如果存在未定义或挂起的步骤,则失败。默认值:`false`

W
wizardforcel 已提交
1717
```java
W
init  
wizardforcel 已提交
1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734
package io.github.bonigarcia;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.extension.ExtendWith;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.jupiter.CucumberExtension;

@CucumberOptions(plugin = { "pretty" })
@ExtendWith(CucumberExtension.class)
public class CucumberTest {

    @TestFactory
    public Stream<DynamicTest> runCukes(Stream<DynamicTest> scenarios) {
W
wizardforcel 已提交
1735
        List<DynamicTest> tests = scenarios.collect(Collectors.toList());
W
init  
wizardforcel 已提交
1736 1737 1738 1739 1740 1741
        return tests.stream();
    }

}
```

W
wizardforcel 已提交
1742
此时,我们可以使用 JUnit5 执行 Cumber 套件。在下面的示例中,我们看到使用 Gradle 运行测试时的输出:
W
init  
wizardforcel 已提交
1743 1744 1745

![](img/00124.gif)

W
wizardforcel 已提交
1746
用Cucumber和青瓜生产 JUnit 5
W
init  
wizardforcel 已提交
1747

W
wizardforcel 已提交
1748
# Docker
W
init  
wizardforcel 已提交
1749

W
wizardforcel 已提交
1750
[Docker](https://www.docker.com/) 是一种开源软件技术,允许将任何应用程序打包并作为轻量级便携容器运行。它提供了一个命令行程序、一个后台守护程序和一组远程服务,简化了容器的生命周期。
W
init  
wizardforcel 已提交
1751

W
wizardforcel 已提交
1752
# 简言之,Docker
W
init  
wizardforcel 已提交
1753

W
wizardforcel 已提交
1754
在历史上,UNIX 风格的操作系统使用“牢狱”一词来描述经过修改的隔离运行时环境。**Linux 容器****LXC**)项目始于 2008 年,将 cGroup、内核名称空间或 chroot(以及其他)集合在一起,以提供完整的隔离执行。LXC 的问题在于它的难度,正因为如此,Docker 技术应运而生。
W
init  
wizardforcel 已提交
1755

W
wizardforcel 已提交
1756
Docker 隐藏在 Linux 内核的上述资源隔离特性(cgroups、内核名称空间等)的底层复杂性中,以允许独立容器在单个 Linux 实例中运行。Docker 提供了一个高级 API,允许将任何应用程序作为容器进行打包、装运和运行。
W
init  
wizardforcel 已提交
1757

W
wizardforcel 已提交
1758
在 Docker 中,容器包含应用程序及其依赖项。多个容器可以在同一台机器上运行,并与其他容器共享同一操作系统内核。每个容器在用户空间中作为独立进程运行。
W
init  
wizardforcel 已提交
1759

W
wizardforcel 已提交
1760
**虚拟机****VM**)不同,在 Docker 容器中不需要使用虚拟机监控程序,虚拟机监控程序是允许创建和运行虚拟机的软件(例如:VirtualBox、VMware、QEMU 或虚拟 PC)。
W
init  
wizardforcel 已提交
1761

W
wizardforcel 已提交
1762
VM 和容器的体系结构如下图所示:
W
init  
wizardforcel 已提交
1763 1764 1765 1766 1767

![](img/00125.jpeg)

虚拟机与容器

W
wizardforcel 已提交
1768
Docker 平台有两个组件:Docker 引擎,负责创建和运行容器;以及 [Docker Hub](https://hub.docker.com/),一种用于分发容器的云服务。Docker Hub 提供了大量可供下载的公共容器映像。Docker 引擎是一个客户机-服务器应用程序,由三个主要组件组成:
W
init  
wizardforcel 已提交
1769 1770

*   作为守护进程(即`dockerd`命令)实现的服务器。
W
wizardforcel 已提交
1771
*   一个 RESTAPI,它指定了程序可以用来与守护进程对话并指示它做什么的接口。
W
wizardforcel 已提交
1772
*   一个**命令行界面****CLI**)客户端(`docker`命令)。
W
init  
wizardforcel 已提交
1773

W
wizardforcel 已提交
1774
# Docker 的 JUnit5 扩展
W
init  
wizardforcel 已提交
1775

W
wizardforcel 已提交
1776
如今,容器正在改变我们开发、分发和运行软件的方式。这对于**持续集成****CI**)测试环境来说尤其有趣,其中与 Docker 的融合直接影响效率的提高。
W
init  
wizardforcel 已提交
1777

W
wizardforcel 已提交
1778
关于 JUnit5,在撰写本文时,Docker 有一个开源 JUnit5 扩展,名为 [JUnit5 Docker](https://faustxvi.github.io/junit5-docker/)。此扩展充当 Docker 引擎的客户端,并允许在运行类测试之前启动 Docker 容器(从 Docker Hub 下载)。该容器在测试结束时停止。为了使用 JUnit5 Docker,首先我们需要在项目中添加依赖项。在 Maven:
W
init  
wizardforcel 已提交
1779

W
wizardforcel 已提交
1780
```java
W
init  
wizardforcel 已提交
1781 1782 1783 1784 1785 1786 1787 1788 1789 1790
<dependency>
   <groupId>com.github.faustxvi</groupId>
   <artifactId>junit5-docker</artifactId>
   <version>${junit5-docker.version}</version>
   <scope>test</scope>
</dependency>
```

在格拉德尔:

W
wizardforcel 已提交
1791
```java
W
init  
wizardforcel 已提交
1792 1793 1794 1795 1796
dependencies {
    testCompile("com.github.faustxvi:junit5-docker:${junitDockerVersion}")
}
```

W
wizardforcel 已提交
1797
JUnit5 Docker 的使用非常简单。我们只需要用`@Docker`注释我们的测试类。此注释中可用的图元如下所示:
W
init  
wizardforcel 已提交
1798

W
wizardforcel 已提交
1799 1800 1801
*   `image`:待启动的 Docker 镜像。
*   `ports`:Docker 容器的端口映射。这是必需的,因为容器必须至少有一个端口可见才能使用。
*   `environments`:传递给 docker 容器的可选环境变量。默认值:`{}`
W
init  
wizardforcel 已提交
1802
*   `waitFor`:运行测试前等待的可选日志。默认值:`@WaitFor(NOTHING)`
W
wizardforcel 已提交
1803
*   `newForEachCase`:布尔标志,用于确定是否应为每个测试用例重新创建容器。如果只为测试类创建一次,则该值将为`false`。默认值:`true`
W
init  
wizardforcel 已提交
1804

W
wizardforcel 已提交
1805
考虑下面的例子。这个测试类使用`@Docker`注释来启动 MySql 容器(容器图像 MySql)和每个测试的开始。内部集装箱端口为`3306`,将映射到主机端口`8801`。然后,定义了几个环境属性(MySql 根密码、默认数据库以及用户名和密码)。直到容器日志中出现跟踪`mysqld:ready for connections`(表示 MySql 实例已启动并正在运行)后,测试才会开始执行。在测试主体中,我们针对容器中运行的 MySQL 实例启动 JDBC 连接。
W
init  
wizardforcel 已提交
1806

W
wizardforcel 已提交
1807
此测试已在 Windows 计算机上执行。因此,JDBCURL 的主机是 192.168.99.100,这是 Docker 机器的 IP。它是一个允许在虚拟主机上安装 Docker 引擎的工具,如 Windows 或 p[Mac](https://docs.docker.com/machine/)。在 Linux 机器中,该 IP 可以是 127.0.0.1(localhost)。
W
init  
wizardforcel 已提交
1808

W
wizardforcel 已提交
1809
```java
W
init  
wizardforcel 已提交
1810
package io.github.bonigarcia;
W
wizardforcel 已提交
1811
 import static org.junit.jupiter.api.Assertions.assertFalse;
W
init  
wizardforcel 已提交
1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831

import java.sql.Connection;
import java.sql.DriverManager;
import org.junit.jupiter.api.Test;
import com.github.junit5docker.Docker;
import com.github.junit5docker.Environment;
import com.github.junit5docker.Port;
import com.github.junit5docker.WaitFor;

@Docker(image = "mysql", ports = @Port(exposed = 8801, inner = 3306), environments = {
        @Environment(key = "MYSQL_ROOT_PASSWORD", value = "root"),
        @Environment(key = "MYSQL_DATABASE", value = "testdb"),
        @Environment(key = "MYSQL_USER", value = "testuser"),
        @Environment(key = "MYSQL_PASSWORD", value = "secret"), }, 
            waitFor = @WaitFor("mysqld: ready for connections"))

public class DockerTest {

    @Test
   void test() throws Exception {
W
wizardforcel 已提交
1832 1833
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection(
W
init  
wizardforcel 已提交
1834 1835
                "jdbc:mysql://192.168.99.100:8801/testdb", "testuser",
                "secret");
W
wizardforcel 已提交
1836
        assertFalse(connection.isClosed());
W
init  
wizardforcel 已提交
1837 1838 1839 1840 1841 1842
        connection.close();
    }

}
```

W
wizardforcel 已提交
1843
在 Docker Windows 终端中执行此测试如下:
W
init  
wizardforcel 已提交
1844 1845 1846

![](img/00126.gif)

W
wizardforcel 已提交
1847
使用 JUnit5 Docker 扩展执行测试
W
init  
wizardforcel 已提交
1848 1849 1850

# 安卓

W
wizardforcel 已提交
1851
[安卓](https://www.android.com/)是基于 Linux 修改版的开源移动操作系统。它最初由一家名为 Android 的初创公司开发,2005 年被谷歌收购并支持。
W
init  
wizardforcel 已提交
1852

W
wizardforcel 已提交
1853
根据 Gartner Inc.(美国 IT 研究和咨询公司)的报告,2017 年,Android 和 iOS 占全球智能手机销量的 99% 以上,如下图所示:
W
init  
wizardforcel 已提交
1854 1855 1856

![](img/00127.jpeg)

W
wizardforcel 已提交
1857
智能手机操作系统市场。图片由 www.statista.com 创建。
W
init  
wizardforcel 已提交
1858 1859 1860

# 简而言之,Android

W
wizardforcel 已提交
1861
Android 是一个基于 Linux 的软件栈,分为若干层。这些层(从下到上)如下所示:
W
init  
wizardforcel 已提交
1862

W
wizardforcel 已提交
1863 1864
*   **Linux 内核**:这是 Android 平台的基础。该层包含 Android 设备各种硬件组件的所有低级设备驱动程序。
*   **硬件抽象层****HAL**):该层提供标准接口,向更高级别的 Java API 框架公开硬件功能。
W
wizardforcel 已提交
1865
*   **安卓运行时****ART**):它为`.dex`文件提供了一个运行时环境,一种字节码格式,旨在减少内存占用。ART 是 Android 5.0 的第一个版本(见下表)。在该版本之前,Dalvik 是 Android 运行时。
W
wizardforcel 已提交
1866
*   **原生 C/C++ 库**:该层包含用 C 和 C++ 编写的原生库,如用于高性能 2D 和 3D 图形处理的 OpenGL ES。
W
wizardforcel 已提交
1867 1868
*   **Java API 框架**:Android 的整个功能集可以通过 Java 编写的 API 提供给开发者。这些 API 是创建 Android 应用程序的构建块,例如:视图系统(用于应用程序 UI)、资源管理器(用于 I18N、图形、布局)、通知管理器(用于状态栏中的自定义警报)、活动管理器(用于管理应用程序生命周期)或内容提供商(启用应用程序从其他应用程序(如联系人等)访问数据)。
*   **应用**:Android 自带一套核心应用,如手机、通讯录、浏览器等。此外,还可以从 Google Play(以前的 Android Market)下载和安装许多其他应用程序:
W
init  
wizardforcel 已提交
1869 1870 1871 1872 1873

![](img/00128.jpeg)

Android layered architecture

W
wizardforcel 已提交
1874
Android 自第一次发布以来已经经历了很多次更新,如下表所示:
W
init  
wizardforcel 已提交
1875

W
wizardforcel 已提交
1876
| **安卓版本** | **代号** | **API 等级** | **Linux 内核版本** | **发布日期** |
W
wizardforcel 已提交
1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890
| --- | --- | --- | --- | --- |
| 1.5 | Cupcake | 3. | 2.6.27 | 2009 年 4 月 30 日 |
| 1.6 | Donut | 4. | 2.6.29 | 2009 年 9 月 15 日 |
| 2.0, 2.1 | Eclair | 5, 6, 7 | 2.6.29 | 2009 年 10 月 26 日 |
| 2.2 | Froyo | 8 | 2.6.32 | 2010 年 5 月 20 日 |
| 2.3 | Gingerbread | 9, 10 | 2.6.35 | 2010 年 12 月 6 日 |
| 3.0, 3.1, 3.2 | Honeycomb | 11, 12, 13 | 2.6.36 | 2011 年 2 月 22 日 |
| 4 | IceCreamSandwich | 14, 15 | 3.0.1 | 2011 年 10 月 18 日 |
| 4.1, 4.2, 4.3 | JellyBean | 16, 17, 18 | 3.0.31, 3.0.21, 3.4.0 | 2012 年 7 月 9 日 |
| 4.4 | KitKat | 19, 20 | 3.10 | 2013 年 10 月 31 日 |
| 5.0, 5.1 | Lollipop | 21, 22 | 3.16.1 | 2014 年 11 月 12 日 |
| 6 | Marshmallow | 23 | 3.18.10 | 2015 年 10 月 5 日 |
| 7.0, 7.1 | Nougat | 24, 25 | 4.4.1 | 2016 年 8 月 22 日 |
| 8 | AndroidO | 26 | TBA | TBA |
W
init  
wizardforcel 已提交
1891

W
wizardforcel 已提交
1892
从开发人员的角度来看,Android 提供了丰富的应用程序框架,允许为移动设备构建应用程序。Android 应用程序是用 Java 编程语言编写的。安卓**软件开发工具包****SDK**)将 Java 代码连同任何数据和资源文件编译成`.apk`(安卓软件包)文件,该文件包含可安装在安卓驱动的设备中,如智能手机、平板电脑、智能电视或智能手表。
W
init  
wizardforcel 已提交
1893

W
wizardforcel 已提交
1894
有关 Android 开发的完整信息,请访问[这里](https://developer.android.com/)
W
init  
wizardforcel 已提交
1895

W
wizardforcel 已提交
1896
Android Studio 是 Android 开发的官方 IDE。它是基于 IntelliJ 理念构建的。在 Android Studio 中,Android 项目的构建过程由 Gradle 构建系统管理。在 Android Studio 安装期间,还可以安装两个附加工具:
W
init  
wizardforcel 已提交
1897

W
wizardforcel 已提交
1898 1899
*   **Android SDK**:包含开发 Android 应用程序所需的所有软件包和工具。SDK 管理器允许下载和安装不同版本的 SDK(请参阅上表)。
*   **Android 虚拟设备****AVD**):这是一个模拟器,允许我们对实际设备进行建模。AVD 管理器允许下载和安装不同的模拟 Android 虚拟设备,这些设备分为四类:手机、桌子、电视和手机。
W
init  
wizardforcel 已提交
1900

W
wizardforcel 已提交
1901
# Android 项目中 JUnit5 的 Gradle 插件
W
init  
wizardforcel 已提交
1902

W
wizardforcel 已提交
1903
在撰写本文时,Android 项目中还没有对 JUnit5 的官方支持。为了解决这个问题,已经创建了一个名为`android-junit5`[开源 Gradle 插件](https://github.com/aurae/android-junit5)。要使用此插件,首先我们需要在`build.gradle`文件中指定适当的依赖项:
W
init  
wizardforcel 已提交
1904

W
wizardforcel 已提交
1905
```java
W
init  
wizardforcel 已提交
1906 1907 1908 1909 1910 1911 1912 1913 1914
buildscript {
    dependencies {
        classpath "de.mannodermaus.gradle.plugins:android-junit5:1.0.0"
    }
}
```

为了在我们的项目中使用此插件,我们需要使用我们的`build.gradle`文件中的条款`apply plugin`来扩展我们的项目功能:

W
wizardforcel 已提交
1915
```java
W
init  
wizardforcel 已提交
1916 1917 1918 1919 1920 1921 1922 1923
apply plugin: "com.android.application"
apply plugin: "de.mannodermaus.android-junit5"

dependencies {
    testCompile junitJupiter()
}
```

W
wizardforcel 已提交
1924
`android-junit5`插件配置`junitPlatform`任务,在测试执行阶段自动连接 Jupiter 和 Vintage 引擎。作为一个示例,考虑以下项目示例,通常托管在 [GitHub](https://github.com/bonigarcia/mastering-junit5/tree/master/junit5-android) 上。以下是在 Android Studio 中导入的该项目的屏幕截图:
W
init  
wizardforcel 已提交
1925 1926 1927 1928 1929

![](img/00129.jpeg)

Android project compatible with JUnit 5 on IntelliJ

W
wizardforcel 已提交
1930
现在,我们将创建 Android Studio 的 Android JUnit 运行配置。如屏幕截图所示,我们使用选项`All in package`引用包含测试的包(本例中为`io.github.bonigarcia.myapplication`
W
init  
wizardforcel 已提交
1931 1932 1933

![](img/00130.jpeg)

W
wizardforcel 已提交
1934
Android JUnit 运行配置
W
init  
wizardforcel 已提交
1935

W
wizardforcel 已提交
1936
如果我们启动上述运行配置,项目的所有测试都将执行。这些测试可以无缝地使用 JUnit 4 编程模型(Vintage)甚至 JUnit 5(Jupiter):
W
init  
wizardforcel 已提交
1937 1938 1939

![](img/00131.jpeg)

W
wizardforcel 已提交
1940
在 IntelliJ 的 Android 项目中执行 Jupiter 和 Vintage 测试
W
init  
wizardforcel 已提交
1941 1942 1943

# 休息

W
wizardforcel 已提交
1944
罗伊·菲尔丁是 1965 年出生的美国计算机科学家。他是 HTTP 协议的作者之一,也是 ApacheWeb 服务器的共同作者。2000 年,菲尔丁在他的博士论文《体系结构风格与基于网络的软件体系结构的设计》中创造了术语 REST(表述性状态转移的缩写)。REST 是设计分布式系统的一种架构风格。这不是一个标准,而是一组约束。REST 通常与 HTTP 结合使用。一方面,遵循 REST 严格原则的实现通常被称为 RESTful。另一方面,那些遵循这些原则的人被称为 RESTlike。
W
init  
wizardforcel 已提交
1945 1946 1947

# 一言以蔽之

W
wizardforcel 已提交
1948
REST 遵循客户机-服务器体系结构。服务器负责处理一组服务,监听客户端发出的请求。客户机和服务器之间的通信必须是无状态的,这意味着服务器不存储来自客户机的任何记录,因此来自客户机的每个请求必须包含服务器单独处理它所需的所有信息。
W
init  
wizardforcel 已提交
1949

W
wizardforcel 已提交
1950
REST 体系结构的构建块称为资源。资源定义要传输的信息类型。应以独特的方式确定资源。在 HTTP 中,访问资源的方法是提供其完整的 URL,也称为 API 端点。每个资源都有一个表示,它是对资源当前状态的机器可读解释。现在,表示通常使用 JSON,但也可以使用其他格式,如 XML 或 YAML。
W
init  
wizardforcel 已提交
1951

W
wizardforcel 已提交
1952
一旦我们确定了资源和表示格式,我们就需要指定可以使用它们做什么,即操作。尽管任何面向资源的系统都应该提供一组常见的操作:CRUD(创建、检索、更新和删除)操作,但操作可能是任何东西。REST 操作可以映射到 HTTP 方法(所谓的谓词),如下所示:
W
init  
wizardforcel 已提交
1953 1954 1955 1956 1957 1958 1959 1960 1961

*   `GET`:读取资源。
*   `POST`:向服务器发送新资源。
*   `PUT`:更新给定资源。
*   `DELETE`:删除一个资源。
*   `PATCH`:部分更新资源。
*   `HEAD`:询问给定资源是否存在,但不返回其任何表示。
*   `OPTIONS`:检索给定资源上可用动词的列表。

W
wizardforcel 已提交
1962
在 REST 中,*幂等性*的概念很重要。例如,`GET``DELETE``PUT`被称为幂等,因为无论命令发送一次还是多次,这些请求的效果都应该相同。另一方面,`POST`不是幂等的,因为每次请求时它都会创建不同的资源。
W
init  
wizardforcel 已提交
1963

W
wizardforcel 已提交
1964
基于 HTTP 的 REST 可以利用标准 HTTP 状态代码。状态代码是一个数字,它总结了与之相关的响应。REST 中重用的典型 HTTP 状态代码有:
W
init  
wizardforcel 已提交
1965

W
wizardforcel 已提交
1966 1967
*   `200 OK`:请求顺利,请求内容已返回。通常用于 GET 请求。
*   `201 Created`:资源已创建。对 POST 或 PUT 请求的响应非常有用。
W
init  
wizardforcel 已提交
1968 1969 1970 1971
*   `204 No content`:操作成功,但没有返回内容。对于不需要响应主体的操作非常有用,例如删除。
*   `301 Moved permanently`:此资源已移动到另一个位置,并返回该位置。
*   `400 Bad request`:发出的请求存在问题(例如缺少一些必需的参数)。
*   `401 Unauthorized`:当拥有请求的用户无法访问请求的资源时,用于身份验证。
W
wizardforcel 已提交
1972 1973 1974
*   `403 Forbidden`:资源不可访问,但与 401 不同,认证不会影响响应。
*   `404 Not found`:提供的 URL 未标识任何资源。
*   405 方法不允许。不允许在资源上使用 HTTP 谓词。(例如,放置在只读资源上)。
W
init  
wizardforcel 已提交
1975 1976
*   `500 Internal server error`:服务器端出现意外情况时的一般错误代码。

W
wizardforcel 已提交
1977
下图显示了客户机-服务器与 REST 交互的示例。HTTP 消息体对请求和响应都使用 JSON:
W
init  
wizardforcel 已提交
1978 1979 1980 1981 1982

![](img/00132.jpeg)

REST sequence diagram example

W
wizardforcel 已提交
1983
# 与 Jupiter 一起使用 REST 测试库
W
init  
wizardforcel 已提交
1984

W
wizardforcel 已提交
1985
RESTAPI 现在变得越来越普及。因此,评估 REST 服务的适当策略是可取的。在本节中,我们将学习如何在 JUnit5 测试中使用几个测试库。
W
init  
wizardforcel 已提交
1986

W
wizardforcel 已提交
1987
首先,我们可以放心使用[开源库](http://rest-assured.io/)。REST Assured 允许通过受 Ruby 或 Groovy 等动态语言启发的流畅 API 验证 REST 服务。要在测试项目中使用 REST Assured,我们只需在 Maven 中添加适当的依赖项:
W
init  
wizardforcel 已提交
1988

W
wizardforcel 已提交
1989
```java
W
init  
wizardforcel 已提交
1990 1991 1992 1993 1994 1995 1996 1997 1998 1999
<dependency>
   <groupId>io.rest-assured</groupId>
   <artifactId>rest-assured</artifactId>
   <version>${rest-assured.version}</version>
   <scope>test</scope>
</dependency>
```

或者在格拉德尔:

W
wizardforcel 已提交
2000
```java
W
init  
wizardforcel 已提交
2001 2002 2003 2004 2005
dependencies {
    testCompile("io.rest-assured:rest-assured:${restAssuredVersion}")
}
```

W
wizardforcel 已提交
2006
之后,我们可以使用 REST-Assured API。下面的类包含两个测试示例。首先向[免费在线 REST 服务](http://echo.jsontest.com/)发送请求。然后验证响应代码和正文内容是否符合预期。第二个测试使用另一个[免费在线 REST 服务](http://services.groupkt.com/),并验证响应:
W
init  
wizardforcel 已提交
2007

W
wizardforcel 已提交
2008
```java
W
init  
wizardforcel 已提交
2009 2010
package io.github.bonigarcia;

W
wizardforcel 已提交
2011 2012
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
W
init  
wizardforcel 已提交
2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027

import org.junit.jupiter.api.Test;
 public class PublicRestServicesTest {

    @Test
    void testEchoService() {
        String key = "foo";
        String value = "bar";
        given().when().get("http://echo.jsontest.com/" + key + "/" + value)
                .then().assertThat().statusCode(200).body(key, 
                equalTo(value));
    }

    @Test
    void testCountryService() {
W
wizardforcel 已提交
2028
        given().when()
W
init  
wizardforcel 已提交
2029 2030
                .get("http://services.groupkt.com/country/get/iso2code/ES")
                .then().assertThat().statusCode(200)
W
wizardforcel 已提交
2031
                .body("RestResponse.result.name", equalTo("Spain"));
W
init  
wizardforcel 已提交
2032 2033 2034 2035 2036
    }

}
```

W
wizardforcel 已提交
2037
使用 Maven 在控制台中运行此测试,我们可以检查两个测试是否成功:
W
init  
wizardforcel 已提交
2038 2039 2040

![](img/00133.gif)

W
wizardforcel 已提交
2041
使用 REST-Assured 执行测试
W
init  
wizardforcel 已提交
2042

W
wizardforcel 已提交
2043
在第二个示例中,我们将研究,除了测试之外,我们还将实现服务器端,即 REST 服务实现。为此,我们将使用本章前面介绍的 SpringMVC 和 SpringBoot(参见“Spring”一节)。
W
init  
wizardforcel 已提交
2044

W
wizardforcel 已提交
2045
在 Spring 中实现 REST 服务非常简单。首先,我们只需要用`@RestController`注释一个 Java 类。在这个类的主体中,我们需要添加带有`@RequestMapping`注释的方法。这些方法将侦听 RESTAPI 中实现的不同 URL(端点)。`@RequestMapping`的可接受要素为:
W
init  
wizardforcel 已提交
2046

W
wizardforcel 已提交
2047 2048
*   `value`:这是路径映射 URL。
*   `method`:查找要映射到的 HTTP 请求方法。
W
init  
wizardforcel 已提交
2049
*   `params`:查找映射请求的参数,缩小主映射范围。
W
wizardforcel 已提交
2050
*   `headers`:his 查找映射请求的头。
W
init  
wizardforcel 已提交
2051 2052 2053 2054 2055
*   `consumes`:查找映射请求的可消耗介质类型。
*   `produces`:查找映射请求的可生产媒体类型。

通过查看以下类的代码可以看出,我们的服务示例实现了三种不同的操作:`GET /books`(读取系统中的所有书籍)、`GET /book/{index}`(读取给定标识符的书籍)和`POST /book`(创建书籍)。

W
wizardforcel 已提交
2056
```java
W
init  
wizardforcel 已提交
2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073
package io.github.bonigarcia;
 import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyRestController {

    @Autowired
    private LibraryService libraryService;

W
wizardforcel 已提交
2074
    @RequestMapping(value = "/books", method = RequestMethod.GET)
W
init  
wizardforcel 已提交
2075 2076 2077 2078
    public List<Book> getBooks() {
        return libraryService.getBooks();
    }

W
wizardforcel 已提交
2079
    @RequestMapping(value = "/book/{index}", method = RequestMethod.GET)
W
init  
wizardforcel 已提交
2080 2081 2082 2083
    public Book getTeam(@PathVariable("index") int index) {
        return libraryService.getBook(index);
    }

W
wizardforcel 已提交
2084
    @RequestMapping(value = "/book", method = RequestMethod.POST)
W
init  
wizardforcel 已提交
2085 2086
    public ResponseEntity<Boolean> addBook(@RequestBody Book book) {
        libraryService.addBook(book);
W
wizardforcel 已提交
2087
        return new ResponseEntity<Boolean>(true, HttpStatus.CREATED);
W
init  
wizardforcel 已提交
2088 2089 2090 2091 2092
    }

}
```

W
wizardforcel 已提交
2093
因为我们正在为 Spring 实现 Jupiter 测试,所以我们需要使用`SpringExtension``SpringBootTest`注释。作为创新,我们将注入一个由`spring-test`提供的测试组件,名为`TestRestTemplate`
W
init  
wizardforcel 已提交
2094

W
wizardforcel 已提交
2095
这个组件是标准 Spring 的`RestTemplate`对象的包装器,它允许以无缝方式实现 REST 客户机。在我们的测试中,它请求我们的服务(在执行测试之前启动),并使用响应来验证结果。
W
init  
wizardforcel 已提交
2096

W
wizardforcel 已提交
2097
请注意,对象`MockMvc`(在“Spring”一节中解释)也可以用于测试 REST 服务。`TestRestTemplate`的区别在于前者用于从客户端(即响应代码、主体、内容类型等)测试,后者用于从服务器端测试服务。例如,在这里的示例中,对服务调用(`getForEntity``postForEntity`的响应是 Java 对象,其作用域仅为服务器端(在客户端,此信息被序列化为 JSON)。
W
init  
wizardforcel 已提交
2098

W
wizardforcel 已提交
2099
```java
W
init  
wizardforcel 已提交
2100 2101
package io.github.bonigarcia;

W
wizardforcel 已提交
2102 2103 2104 2105
import static org.junit.Assert.assertEquals;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
W
init  
wizardforcel 已提交
2106 2107 2108 2109 2110 2111 2112 2113 2114 2115
 import java.time.LocalDate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
W
wizardforcel 已提交
2116
@SpringBootTest(webEnvironment = RANDOM_PORT)
W
init  
wizardforcel 已提交
2117 2118 2119 2120 2121 2122 2123 2124 2125
class SpringBootRestTest {

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    void testGetAllBooks() {
        ResponseEntity<Book[]> responseEntity = restTemplate
                .getForEntity("/books", Book[].class);
W
wizardforcel 已提交
2126 2127
        assertEquals(OK, responseEntity.getStatusCode());
        assertEquals(3, responseEntity.getBody().length);
W
init  
wizardforcel 已提交
2128 2129 2130 2131 2132 2133
    }

    @Test
    void testGetBook() {
        ResponseEntity<Book> responseEntity = restTemplate
                .getForEntity("/book/0", Book.class);
W
wizardforcel 已提交
2134 2135
        assertEquals(OK, responseEntity.getStatusCode());
        assertEquals("The Hobbit", responseEntity.getBody().getName());
W
init  
wizardforcel 已提交
2136 2137 2138 2139 2140
    }

    @Test
    void testPostBook() {
        Book book = new Book("I, Robot", "Isaac Asimov",
W
wizardforcel 已提交
2141
                LocalDate.of(1950, 12, 2));
W
init  
wizardforcel 已提交
2142 2143
        ResponseEntity<Boolean> responseEntity = restTemplate
                .postForEntity("/book", book, Boolean.class);
W
wizardforcel 已提交
2144 2145
        assertEquals(CREATED, responseEntity.getStatusCode());
        assertEquals(true, responseEntity.getBody());
W
init  
wizardforcel 已提交
2146 2147
        ResponseEntity<Book[]> responseEntity2 = restTemplate
                .getForEntity("/books", Book[].class);
W
wizardforcel 已提交
2148
        assertEquals(responseEntity2.getBody().length, 4);
W
init  
wizardforcel 已提交
2149 2150 2151 2152 2153
    }

}
```

W
wizardforcel 已提交
2154
如下面的屏幕截图所示,我们的 Spring 应用程序在运行测试之前启动,测试成功执行:
W
init  
wizardforcel 已提交
2155 2156 2157

![](img/00134.gif)

W
wizardforcel 已提交
2158
使用 TestRestTemplate 验证 REST 服务的 Jupiter 测试的输出。
W
init  
wizardforcel 已提交
2159

W
wizardforcel 已提交
2160
在本节结束时,我们将看到一个示例,其中使用 [WireMock](http://wiremock.org/) 库。该库允许模拟 REST 服务,即所谓的 HTTP *模拟服务器*。此模拟服务器捕获对服务的传入请求,并提供桩响应。此功能对于测试使用 REST 服务的系统非常有用,但该服务在测试期间不可用(或者我们可以测试单独调用该服务的组件)。
W
init  
wizardforcel 已提交
2161

W
wizardforcel 已提交
2162
像往常一样,我们看到一个示例来演示它的用法。假设我们有一个使用远程 REST 服务的系统。为了实现该服务的客户机,我们使用 [Retrofit2](http://square.github.io/retrofit/),这是一个高度可配置的 Java HTTP 客户端。我们定义了使用此服务的接口,如下面的类所示。请注意,该服务公开了三个用于读取远程文件的端点(打开文件、读取流和关闭流):
W
init  
wizardforcel 已提交
2163

W
wizardforcel 已提交
2164
```java
W
init  
wizardforcel 已提交
2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184
package io.github.bonigarcia;

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.POST;
import retrofit2.http.Path;
 public interface RemoteFileApi {

    @POST("/api/v1/paths/{file}/open-file")
    Call<ResponseBody> openFile(@Path("file") String file);

    @POST("/api/v1/streams/{streamId}/read")
    Call<ResponseBody> readStream(@Path("streamId") String streamId);

    @POST("/api/v1/streams/{streamId}/close")
    Call<ResponseBody> closeStream(@Path("streamId") String streamId);

}
```

W
wizardforcel 已提交
2185
然后我们实现使用 REST 服务的类。在本例中,它是一个简单的 Java 类,它连接到远程服务,并将其 URL 作为构造函数参数传递:
W
init  
wizardforcel 已提交
2186

W
wizardforcel 已提交
2187
```java
W
init  
wizardforcel 已提交
2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202
package io.github.bonigarcia;

import java.io.IOException;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
 public class RemoteFileService {

    private RemoteFileApi remoteFileApi;

    public RemoteFileService(String baseUrl) {
        Retrofit retrofit = new Retrofit.Builder()
W
wizardforcel 已提交
2203 2204
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
W
init  
wizardforcel 已提交
2205 2206 2207 2208 2209 2210 2211 2212
                .baseUrl(baseUrl).build();
        remoteFileApi = retrofit.create(RemoteFileApi.class);
    }

    public byte[] getFile(String file) throws IOException {
        Call<ResponseBody> openFile = remoteFileApi.openFile(file);
        Response<ResponseBody> execute = openFile.execute();
        String streamId = execute.body().string();
W
wizardforcel 已提交
2213
        System.out.println("Stream " + streamId + " open");
W
init  
wizardforcel 已提交
2214 2215 2216

        Call<ResponseBody> readStream = remoteFileApi.readStream(streamId);
        byte[] content = readStream.execute().body().bytes();
W
wizardforcel 已提交
2217
        System.out.println("Received " + content.length + " bytes");
W
init  
wizardforcel 已提交
2218 2219

        remoteFileApi.closeStream(streamId).execute();
W
wizardforcel 已提交
2220
        System.out.println("Stream " + streamId + " closed");
W
init  
wizardforcel 已提交
2221 2222 2223 2224 2225 2226 2227

        return content;
    }

}
```

W
wizardforcel 已提交
2228
最后,我们实现了一个 JUnit5 测试来验证我们的服务。注意,我们正在创建模拟服务器(`new WireMockServer`,并使用 WireMock 在测试设置(`@BeforeEach`中提供的静态方法`stubFor(...)`对 REST 服务调用进行插桩。由于在本例中,SUT 非常简单,并且没有文档,因此我们也在每个测试的设置中直接实例化了类`RemoteFileService`,使用模拟服务器 URL 作为构造函数参数。最后,我们测试我们的服务(使用模拟服务器),在本例中,通过调用方法`getFile`并评估其输出,简单地运行名为`wireMockServer`的对象。
W
init  
wizardforcel 已提交
2229

W
wizardforcel 已提交
2230
```java
W
init  
wizardforcel 已提交
2231 2232 2233

package io.github.bonigarcia;

W
wizardforcel 已提交
2234 2235 2236 2237 2238 2239 2240
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.junit.jupiter.api.Assertions.assertEquals;
W
init  
wizardforcel 已提交
2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268
 import java.io.IOException;
import java.net.ServerSocket;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.github.tomakehurst.wiremock.WireMockServer;

public class RemoteFileTest {

    RemoteFileService remoteFileService;
    WireMockServer wireMockServer;

    // Test data
    String filename = "foo";
    String streamId = "1";
    String contentFile = "dummy";

    @BeforeEach
    void setup() throws Exception {
        // Look for free port for SUT instantiation
        int port;
        try (ServerSocket socket = new ServerSocket(0)) {
            port = socket.getLocalPort();
        }
        remoteFileService = new RemoteFileService("http://localhost:" + 
             port);

        // Mock server
W
wizardforcel 已提交
2269
        wireMockServer = new WireMockServer(options().port(port));
W
init  
wizardforcel 已提交
2270
        wireMockServer.start();
W
wizardforcel 已提交
2271
        configureFor("localhost", wireMockServer.port());
W
init  
wizardforcel 已提交
2272 2273

        // Stubbing service
W
wizardforcel 已提交
2274
        stubFor(post(urlEqualTo("/api/v1/paths/" + filename + "/open-
W
init  
wizardforcel 已提交
2275
           file"))
W
wizardforcel 已提交
2276 2277
           .willReturn(aResponse().withStatus(200).withBody(streamId)));
        stubFor(post(urlEqualTo("/api/v1/streams/" + streamId + 
W
init  
wizardforcel 已提交
2278
           "/read"))
W
wizardforcel 已提交
2279 2280 2281
           .willReturn(aResponse().withStatus(200).withBody(contentFile)));
        stubFor(post(urlEqualTo("/api/v1/streams/" + streamId + /close"))
           .willReturn(aResponse().withStatus(200)));
W
init  
wizardforcel 已提交
2282 2283 2284 2285 2286
    }

    @Test
    void testGetFile() throws IOException {
        byte[] fileContent = remoteFileService.getFile(filename);
W
wizardforcel 已提交
2287
        assertEquals(contentFile.length(), fileContent.length);
W
init  
wizardforcel 已提交
2288 2289 2290 2291 2292 2293 2294 2295 2296 2297
    }

    @AfterEach
    void teardown() {
        wireMockServer.stop();
    }

}
```

W
wizardforcel 已提交
2298
在控制台中执行测试,在跟踪中,我们可以看到 WireMock 控制的内部 HTTP 服务器是如何在测试执行之前启动的。然后,测试执行三个 REST 操作(开放流、读取字节、关闭流),最后释放模拟服务器:
W
init  
wizardforcel 已提交
2299 2300 2301

![](img/00135.gif)

W
wizardforcel 已提交
2302
使用 WireMock 使用模拟 REST 服务器执行测试
W
init  
wizardforcel 已提交
2303 2304 2305

# 总结

W
wizardforcel 已提交
2306
本节详细介绍了 JUnit5 如何与第三方框架、库和平台结合使用。由于 Jupiter 扩展模型,开发人员可以创建扩展,允许与 JUnit5 的外部框架无缝集成。首先,我们看到了`MockitoExtension`,JUnit 5 团队提供了一个扩展,用于在 Jupiter 测试中使用 Mockito(一个臭名昭著的 Java 模拟框架)。然后,我们使用了`SpringExtension`,这是 Spring 框架版本 5 中提供的官方扩展。此扩展将 Spring 集成到 JUnit5 编程模型中。这样,我们就能够在测试中使用 Spring 的应用程序上下文(即 Spring 的 DI 容器)。
W
init  
wizardforcel 已提交
2307

W
wizardforcel 已提交
2308
我们还回顾了`SeleniumExtension`,由 SeleniumJupiter 实施的`SeleniumExtension`是一个开源项目,为 SeleniumWebDriver(Web 应用程序测试框架)提供 JUnit5 扩展。感谢 thins 扩展,我们可以使用不同的浏览器与 Web 应用程序和模拟移动设备(使用 Appium)自动交互。然后,我们看到了`CucumberExtension`,允许使用 Gherkin 语言指定 JUnit 5 验收测试,该测试遵循 BDD 风格。最后,我们已经了解了如何在执行 JUnit5 测试之前使用开源 JUnit5 Docker 扩展来启动 Docker 容器(从 Docker Hub 下载图像)。
W
init  
wizardforcel 已提交
2309

W
wizardforcel 已提交
2310
此外,我们发现扩展模型不是通过 JUnit 测试与外部技术交互的唯一方式。例如,为了在 Android 项目中运行 Jupiter 测试,我们可以使用`android-junit5`插件。另一方面,即使没有使用 JUnit 5 评估 REST 服务的自定义扩展,与此类库的集成也是向前发展的:我们只需要在项目中包含适当的依赖项,并在测试中使用它(例如,REST Assured、Spring 或 WireMock)。