# Test 测试

# Allure 测试报告

# 测试注解

  • @Epic:用于将测试用例分组到“史诗”层次。一个“史诗”是一个大型项目中的关键功能模块,通常需要花费大量时间和资源来开发和测试。
  • @Story:用于将测试用例分组到“故事”层次。一个“故事”是一个用户场景,描述了用户如何使用系统中的一个功能。
  • @Feature:用于将测试用例分组到“功能”层次。一个“功能”是一个系统中的具体功能或模块。

# 测试优先级

  • BLOCKER("blocker"), 阻挡的
  • CRITICAL 批判
  • NORMAL 正常
  • MINOR("minor"), 轻微的
  • TRIVIAL("trivial"); 不重要的
函数 说明 备注
@allure.epic() 敏捷中的概念 项目名称
@allure.feature() 模块名称 模块名
@allure.story() 用户故事 子模块
@allure.title(用例的标题) 用例标题 用例标题
@allure.severity() 用例等级 包括:blocker,critical,normal,minor,trivial
@allure.step() 操作步骤 测试步骤
@allure.description() 测试用例描述 可以写预期结果
@allure.testcase(url) 测试用例链接 链接到测试用例系统
@allure.issue(url) 测试 bug 链接 链接到 bug 系统
@allure.link(url) 链接 一般可以链接到被测系统地址
@allure.attachment() 附件 一般可以添加截图或者日志

# 测试基本库

# 资源

# 原则

单元测试的 FIRST 规则

  • Fast 快速原则,测试的速度要比较快,
    • Independent 独立原则,每个测试用例应该互不影响,不依赖于外部资源。
    • Repeatable 可重复原则,同一个测试用例多次运行的结果应该是相同的
    • Self-validating 自我验证原则,单元测试可以自动验证,并不需要手工干预
    • Thorough 及时原则 单元测试必须即使进行编写,更新,维护。保证测试用例随着业务动态变化
  • AIR 原则
    • Automatic 自动化原则 单元测试应该是自动运行,自动校验,自动给出结果。
    • Independent 独立原则 单元测试应该独立运行,吧相互之间无依赖,对外无依赖,多次运行之间无依赖。
    • Repeatable 可重复原则 单元测试是可重复运动的,每次的结果都稳定可靠。

# 基本概念

框架是一个应用的半成品,框架提供了一个可以复用的公共结构,可以在多个应用程序之间进行共享。

单元测试:单个硬件,软件单元或一组相关单元的测试。确保方法接受预期范围内的输入,并且为每个测试输入返回预期的值。任何没有经过测试的程序功能都可以当作不存在。

一次只能单元测试一个对象

单元测试是一个独立工作单元的行为。集成测试与验收测试检查的是各种组件如何交互。

api 契约是一个观点,把一个应用程序编程接口当作是在调用者与被调用者之间正式的协议。

所有单元测试应该遵循的原则

  • 每个单元测试都必须独立于其它所有单元测试而运行、
  • 框架应该以单个测试为单位来检测和报告错误
  • 应该易于定义要运行哪些单元测试
  • 测试任何有可能失败的事情

测试集{Suite} Junit5 不支持

创建单元测试的原则:

  1. 通过把环境设置成已知状态(如创建对象,获取资源)眯创建测试,测试前的状态称为 Test Fixture
  2. 调用待测试的方法
  3. 确认测试结果,通常通过调用一个或更多个 assert 方法来实现

# 断言

方法 功能
assertArrayEquals 断言两个数组是否相等
assertEquals 断言两个对象是否相等
assertSame 断言两个对象是否同一对象
assertTrue 是否为真
assertNotNull 不是非空

# 常用测试

# 测试异常

Throwable throwable = assertThrows(MyRuntimeException.class, new Thrower()::throwsRuntime);

assertAll(
    () -> assertEquals("My custom runtime exception", throwable.getMessage()),
    () -> assertNull(throwable.getCause())
);

 MyException thrown =
        assertThrows(MyException.class,
           () -> myObject.doThing(),
           "Expected doThing() to throw, but it didn't");

    assertTrue(thrown.getMessage().contains("Stuff")
1
2
3
4
5
6
7
8
9
10
11
12
13

测试超时

@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)

1
2

# Hamcrest

# 一般匹配符

1、allOf

 匹配符表明如果接下来的所有条件必须都成立测试才通过,相当于“与”(&&)
assertThat( testedNumber, allOf( greaterThan(8), lessThan(16) ) );

2、anyOf

匹配符表明如果接下来的所有条件只要有一个成立则测试通过,相当于“或”(||)
assertThat( testedNumber, anyOf( greaterThan(16), lessThan(8) ) );

3、anything

匹配符表明无论什么条件,永远为true
assertThat( testedNumber, anything() );

4、is

匹配符表明如果前面待测的object等于后面给出的object,则测试通过

assertThat( testedString, is( "developerWorks" ) ); 5、not

匹配符和is匹配符正好相反,表明如果前面待测的object不等于后面给出的object,则测试通过
assertThat( testedString, not( "developerWorks" ) );
# 字符串相关匹配符

1、containsString

 匹配符表明如果测试的字符串testedString 包含 子字符串"developerWorks"则测试通过
 assertThat( testedString, containsString( "developerWorks" ) );

2、endsWith

 匹配符表明如果测试的字符串testedString以子字符串"developerWorks"结尾则测试通过
 assertThat( testedString, endsWith( "developerWorks" ) );

3、startsWith

 匹配符表明如果测试的字符串testedString以子字符串"developerWorks"开始则测试通过
 assertThat( testedString, startsWith( "developerWorks" ) );

4、equalTo

 匹配符表明如果测试的testedValue等于expectedValue则测试通过,equalTo可以测试数值之间,字符串

 之间和对象之间是否相等,相当于Object的equals方法
 assertThat( testedValue, equalTo( expectedValue ) );

5、equalToIgnoringCase

 匹配符表明如果测试的字符串testedString在忽略大小写的情况下等于"developerWorks"则测试通过
 assertThat( testedString, equalToIgnoringCase( "developerWorks" ) );

6、equalToIgnoringWhiteSpace

 匹配符表明如果测试的字符串testedString在忽略头尾的任意个空格的情况下等于"developerWorks"则

 测试通过,注意:字符串中的空格不能被忽略
 assertThat( testedString, equalToIgnoringWhiteSpace( "developerWorks" ) );
# 数值相关匹配符

1、closeTo

 匹配符表明如果所测试的浮点型数testedDouble在20.0±0.5范围之内则测试通过
 assertThat( testedDouble, closeTo( 20.0, 0.5 ) );

2、greaterThan

 匹配符表明如果所测试的数值testedNumber大于16.0则测试通过
 assertThat( testedNumber, greaterThan(16.0) );

3、lessThan

 匹配符表明如果所测试的数值testedNumber小于16.0则测试通过
 assertThat( testedNumber, lessThan (16.0) );

4、greaterThanOrEqualTo

 匹配符表明如果所测试的数值testedNumber大于等于16.0则测试通过
 assertThat( testedNumber, greaterThanOrEqualTo (16.0) );

5、lessThanOrEqualTo

 匹配符表明如果所测试的数值testedNumber小于等于16.0则测试通过
 assertThat( testedNumber, lessThanOrEqualTo (16.0) );
# collection 相关匹配符

1、hasEntry

 匹配符表明如果测试的Map对象mapObject含有一个键值为"key"对应元素值为"value"的Entry项则测试通过
 assertThat( mapObject, hasEntry( "key", "value" ) );

2、hasItem

 匹配符表明如果测试的迭代对象iterableObject含有元素“element”项则测试通过
 assertThat( iterableObject, hasItem ( "element" ) );

3、hasKey

 匹配符表明如果测试的Map对象mapObject含有键值“key”则测试通过
 assertThat( mapObject, hasKey ( "key" ) );

4、hasValue

 匹配符表明如果测试的Map对象mapObject含有元素值“value”则测试通过
 assertThat( mapObject, hasValue ( "key" ) );

# mockito

在 junit5 中

testImplementation "org.mockito:mockito-junit-jupiter:3.9.0"
1
ExtendWith(MockitoExtension.class)
class MybatisPlusFunctionTest  {


    @Autowired
    private UserService userService;


    @Mock
    private PermissionService permissionService;

    @Test
    @Order(2)
    @DisplayName("2、mybatis 无结果返回时,不为空,容器为 0")
    void test_get_permission_by_user_id_not() {
        List<Permission> list= new ArrayList<>();
        list.add(new Permission());
        when(permissionService.getByUserId(1L)).thenReturn(list);
        List<Permission> permissionList = permissionService.getByUserId(1L);
        assertThat(permissionList, is(notNullValue()));
        assertThat("数据为0",permissionList.size(), equalTo(1));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# FAKER

https://github.com/DiUS/java-faker

https://java-faker.herokuapp.com/

  • FAKER.name().fullName();
  • FAKER.number().randomDouble(2, 1, 100) 1~100 之间两位小数数字
public static List<Student> listStudentList(final int number) {
	return Stream.generate(() -> new Student(
        FAKER.name().fullName(),
        FAKER.number().randomDouble(2, 1, number)
    )).limit(number).collect(Collectors.toList());
}

1
2
3
4
5
6
7

# Junit5

// 替换了Junit4中的RunWith和Rule
@ExtendWith(SpringExtension.class)
//提供spring依赖注入
@SpringBootTest
// 运行单元测试时显示的名称
@DisplayName("Test MerchantController")
// 单元测试时基于的配置文件
@TestPropertySource(locations = "classpath:ut-bootstrap.yml")
class MerchantControllerTest{
    private static RedisServer server = null;

    // 下面三个mock对象是由spring提供的
    @Resource
    MockHttpServletRequest request;

    @Resource
    MockHttpSession session;

    @Resource
    MockHttpServletResponse response;

    // junit4中 @BeforeClass
    @BeforeAll
    static void initAll() throws IOException {
        server = RedisServer.newRedisServer(9379);
        server.start();
    }


    // junit4中@Before
    @BeforeEach
    void init() {
        request.addHeader("token", "test_token");
    }

    // junit4中@After
    @AfterEach
    void tearDown() {
    }

    // junit4中@AfterClass
    @AfterAll
    static void tearDownAll() {
        server.stop();
        server = null;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# JUnit 5 架构体系

与 JUnit 4 不同,JUnit 5 不再是单个库,而是模块化结构的集合,JUnit 5 由三个不同的子项目组成

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  1. JUnit Platform JUnit 平台作为在 JVM 上启动测试框架的基础。 它还定义了用于开发在平台上运行的测试框架的 TestEngine API。 此外,该平台提供了一个控制台启动器,从命令行启动平台,为 Gradle 和 Maven 构建插件以及基于 JUnit 4 的 Runner,用于在平台上运行任何 TestEngine。
  2. JUnit Jupiter JUnit Jupiter 是用于在 JUnit 5 中编写测试和扩展的新编程模型和扩展模型的组合.Jupiter 子项目提供了一个用于在平台上运行基于 Jupiter 的测试的 TestEngine。
  3. JUnit Vintage JUnit Vintage 提供了一个用于在平台上运行 JUnit 3 和 JUnit 4 的测试的 TestEngine。

# JUnit 5 注解

JUnit 5 提供了以下注释来编写测试。

  • @BeforeEach 注释方法将在测试类中的每个测试方法之前运行。
  • @AfterEach 注释方法将在测试类中的每个测试方法之后运行。
  • @BeforeAll 注释方法将在测试类中的所有测试方法之前运行。 此方法必须是静态的。
  • @AfterAll 注释方法将在测试类中的所有测试方法之后运行。 此方法必须是静态的。
  • @Test 它用于将方法标记为 junit 测试
  • @DisplayName 用于为测试类或测试方法提供任何自定义显示名称
  • @Disable 它用于禁用或忽略来自测试套件的测试类或方法。
  • @Nested 用于创建嵌套测试类
  • @Tag 使用用于测试发现和过滤的标签来标记测试方法或测试类
  • @TestFactory 标记方法是动态测试的测试工厂

# 使用 JUnit 5 中编写动态测试

JUnit 4 和 JUnit 5 在测试写作风格中没有太大变化。 这里是他们的生命周期方法的样本测试。

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import com.howtodoinjava.junit5.examples.Calculator;

public class AppTest {

    @BeforeAll
    static void setup(){
        System.out.println("@BeforeAll executed");
    }

    @BeforeEach
    void setupThis(){
        System.out.println("@BeforeEach executed");
    }

    @Tag("DEV")
    @Test
    void testCalcOne()
    {
        System.out.println("======TEST ONE EXECUTED=======");
        Assertions.assertEquals( 4 , Calculator.add(2, 2));
    }

    @Tag("PROD")
    @Disabled
    @Test
    void testCalcTwo()
    {
        System.out.println("======TEST TWO EXECUTED=======");
        Assertions.assertEquals( 6 , Calculator.add(2, 4));
    }

    @AfterEach
    void tearThis(){
        System.out.println("@AfterEach executed");
    }

    @AfterAll
    static void tear(){
        System.out.println("@AfterAll executed");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 测试套件

使用 JUnit 5 测试套件,您可以将测试扩展到多个测试类和不同的软件包。 JUnit 5 提供了两个注解:@SelectPackages 和@SelectClasses 来创建测试套件。 要执行该套件,您将使用@RunWith(JUnitPlatform.class)。

@SelectPackages("com.howtodoinjava.junit5.examples")
public class JUnit5TestSuiteExample
{
}
1
2
3
4

此外,您可以使用以下注解来过滤测试包,类甚至测试方法。 1.@IncludePackages 和 @ExcludePackages 过滤测试包 2.@IncludeClassNamePatterns 和 @ExcludeClassNamePatterns 过滤测试类 3.@IncludeTags 和 @ExcludeTags 过滤测试方法

 @RunWith(JUnitPlatform.class)
@SelectPackages("com.howtodoinjava.junit5.examples")
@IncludePackages("com.howtodoinjava.junit5.examples.packageC")
@ExcludeTags("PROD")
public class JUnit5TestSuiteExample
{
}
1
2
3
4
5
6
7

# Junit5 中的断言

断言有助于使用测试用例的实际输出验证预期输出。 为了保持简单,所有 JUnit Jupiter 断言是 org.junit.jupiter.Assertions 类中的静态方法,例如 assertEquals(),assertNotEquals()。

void testCase()
{
    //Test will pass
    Assertions.assertNotEquals(3, Calculator.add(2, 2));
    //Test will fail
    Assertions.assertNotEquals(4, Calculator.add(2, 2), "Calculator.add(2, 2) test failed");
    //Test will fail
    Supplier<String> messageSupplier  = ()-> "Calculator.add(2, 2) test failed";
    Assertions.assertNotEquals(4, Calculator.add(2, 2), messageSupplier);
}
1
2
3
4
5
6
7
8
9
10

# 假设

Assumptions 类提供了静态方法来支持基于假设的条件测试执行。 失败的假设导致测试被中止。 无论何时继续执行给定的测试方法没有意义,通常使用假设。 在测试报告中,这些测试将被标记为已通过。 JUnit jupiter Assumptions 类有两个这样的方法:putsFalse(),putsTrue()。

 class AppTest {
    @Test
    void testOnDev()
    {
        System.setProperty("ENV", "DEV");
        Assumptions.assumeTrue("DEV".equals(System.getProperty("ENV")), AppTest::message);
    }

    @Test
    void testOnProd()
    {
        System.setProperty("ENV", "PROD");
        Assumptions.assumeFalse("DEV".equals(System.getProperty("ENV")));
    }
     private static String message () {
        return "TEST Execution Failed :: ";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 测试 Controller

# Mork 对象

Mockito 是 Java 单元测试中使用率最高的 Mock 框架之一。它通过简明的语法和完整的文档吸引了大量的开发者。Mockito 支持用 Maven 和 Gradle 来进行依赖引入和管理。这里只给出 Maven 中引入依赖的例子:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <scope>test</scope>
</dependency>
1
2
3
4
5

# 方法 1. Mockito.mock

直接使用 Mockito 提供的 mock 方法即可以模拟出一个服务的实例。再结合 when/thenReturn 等语法完成方法的模拟实现。

import static org.mockito.Mockito.*;

@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class MockDemo1 {

    private DemoService demoService;

    @Before
    public void before() {
        demoService = mock(DemoService.class);
    }

    @Test
    public void test() {
        when(demoService.hello()).thenReturn("hello my friend");
        System.out.println(demoService.hello());
        verify(demoService).hello();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 方法 2. MockitoAnnotations.initMocks(this)

这里给出了使用@Mock 注解来 Mock 对象时的第一种实现,即使用 MockitoAnnotations.initMocks(testClass)。

@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class MockDemo2 {

    @Mock
    private DemoService demoService;

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void test() {
        when(demoService.hello()).thenReturn("hello rale");
        System.out.println(demoService.hello());
        verify(demoService).hello();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 方法 3. @RunWith(MockitoJUnitRunner.class)(推荐)

@RunWith(MockitoJUnitRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class MockDemo3 {

    @Mock
    private DemoService demoService;

    @Test
    public void test() {
        when(demoService.hello()).thenReturn("hello rale");
        System.out.println(demoService.hello());
        verify(demoService).hello();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这里需要注意的是如果使用 MockitoRule 的话,该对象的访问级别必须为 public。

# 方法 4. MockitoRule

这里需要注意的是如果使用 MockitoRule 的话,该对象的访问级别必须为 public。

@RunWith(JUnit4.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class MockDemo4 {

    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Mock
    private DemoService demoService;

    @Test
    public void test() {
        when(demoService.hello()).thenReturn("hello rale");
        System.out.println(demoService.hello());
        verify(demoService).hello();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Stub

标准的 Stub 在上文中已经给出了简单的例子,目前 Mockito 基于 BDD(Behavior Driven Development)的思想还提供了类似的 given/willReturn 的语法。但是,Spring 同样作为 IOC 框架,和 Mockito 的融合存在一定的问题。即如果需要对 Spring Bean 中的部分依赖进行 Stub 时,需要手动的去设置。

Mockito 其实提供了一个非常方便的注解叫做@InjectMocks,该注解会自动把该单元测试中声明的 Mock 对象注入到该 Bean 中。但是,我在实验的过程中遇到了问题,即@InjectMocks 如果想要标记在接口上,则该接口必须手动初始化,否则会抛出无法初始化接口的异常。但是,如果不使用 Spring 的自动注入,则必须手动的将该类依赖的别的 Bean 注入进去。

因此目前使用 Mockito 的妥协方案是直接@Autowire 该接口的实现。然后在上面标记 InjectMocks 注解,此时会将测试中声明的 Mock 对象自动注入,而没有声明的依赖的对象依然采用 Spring Bean 的依赖注入:

@RunWith(MockitoJUnitRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class InjectMockTest {

    @Mock
    private WelcomeLanguageService welcomeLanguageService;

    @Autowired
    @InjectMocks
    private DemoServiceImpl demoService;

    @Before4
    public void before() {
        MockitoAnnotations.initMocks(this);
        given(welcomeLanguageService.welcome()).willReturn("hahaha");
    }
    @Test
    public void test() {
        System.out.println(demoService.hello());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeController.class)
public class EmployeeController2Test {
    @Autowired
    private MockMvc mvc;

    @MockBean
    private EmployeeService employeeService;

    public void setUp() {
        // 数据打桩,设置该方法返回的 body一直 是空的
        Mockito.when(employeeService.findEmployee()).thenReturn(new ArrayList<>());
    }

    @Test
    public void listAll() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/emp"))
                .andExpect(status().isOk()) // 期待返回状态吗码200
                // JsonPath expression  https://github.com/jayway/JsonPath
                //.andExpect(jsonPath("$[1].name").exists()) // 这里是期待返回值是数组,并且第二个值的 name 存在,所以这里测试是失败的
                .andDo(print()); // 打印返回的 http response 信息
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Spring 中的测试

# 通过 TestRestTemplate 进行单元测试

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class IndexControllerTest {


    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void indexTest() {
        ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class);
        assertThat(entity.getStatusCode(), equalTo(HttpStatus.OK));

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# OAuto2 Resource 单元测试

when(resourceServerTokenServices.loadAuthentication(anyString())).thenAnswer(
        invocation -> SecurityContextHolder.getContext().getAuthentication());
1
2
 OAuth2Request(Map<String, String> requestParameters, String clientId,
      Collection<? extends GrantedAuthority> authorities, boolean approved, Set<String> scope,
      Set<String> resourceIds, String redirectUri, Set<String> responseTypes,
      Map<String, Serializable> extensionProperties
1
2
3
4

# springboot~WebTestClient 的使用


webTestClient.post().uri("/api/repos")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .body(Mono.just(repoRequest), RepoRequest.class)
    .exchange()
    .expectStatus().isOk()
    .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
    .expectBody()
    .jsonPath("$.name").isNotEmpty()
    .jsonPath("$.name").isEqualTo("test-webclient-repository");


webTestClient.get().uri("/api/repos")
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .exchange()
    .expectStatus().isOk()
    .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
    .expectBodyList(GithubRepo.class);

webTestClient.get()
    .uri("/api/repos/{repo}", "test-webclient-repository")
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .consumeWith(response ->
        Assertions.assertThat(response.getResponseBody()).isNotNull());

RepoRequest newRepoDetails = new RepoRequest("updated-webclient-repository", "Updated name and description");
webTestClient.patch()
    .uri("/api/repos/{repo}", "test-webclient-repository")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .body(Mono.just(newRepoDetails), RepoRequest.class)
    .exchange()
    .expectStatus().isOk()
    .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
    .expectBody()
    .jsonPath("$.name").isEqualTo("updated-webclient-repository");

webTestClient.delete()
    .uri("/api/repos/{repo}", "updated-webclient-repository")
    .exchange()
    .expectStatus().isOk();


client.get().uri("/persons")
    .exchange()
    .expectStatus().isOk()
    .expectBodyList(Person.class).hasSize(3).contains(person);

EntityExchangeResult<Person> result = client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody(Person.class)
        .returnResult();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

如果响应没有内容(或者你不在乎),请使用 Void.class,以确保释放资源。以下示例显示了如何执行此操作:


client.get().uri("/persons/123")
        .exchange()
        .expectStatus().isNotFound()
        .expectBody(Void.class);
1
2
3
4
5

当你使用 ExpectBody()时,响应以 byte[]的形式使用。这对于原始内容声明很有用。例如,你可以使用 JSONAssert 来验证 JSON 内容,如下所示:

client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .json("{\"name\":\"Jane\"}")
1
2
3
4
5

你还可以使用 JSONPath 表达式,如下所示:

client.get().uri("/persons")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .jsonPath("$[0].name").isEqualTo("Jane")
        .jsonPath("$[1].name").isEqualTo("Jason");
1
2
3
4
5
6

上传文件

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestMerchantRequire {

    protected WebTestClient client;

    @Autowired
    private ApplicationContext axt;


    @Before
    public void initial() {
        client = WebTestClient.bindToApplicationContext(axt).configureClient().defaultHeader("tokenName", "token").build();
    }

    @Test
    public void testMerchantRequired() {
        String fileBase64 = "data:image/jpg;base64,图片的base64 码";
        String uri = "/uploadFile";
        MultipartBodyBuilder builder = new MultipartBodyBuilder();
        builder.part("image", Base64.getDecoder().decode(fileBase64.replace("data:image/jpg;base64,", "")))
            .header("Content-Disposition", "form-data; name=files; filename=image.jpg");

        WebTestClient.ResponseSpec body = client.post()
            .uri(uri)
            .contentType(MediaType.MULTIPART_FORM_DATA)
            .body(BodyInserters.fromMultipartData(builder.build()))
            .exchange()
            .expectStatus()
            .isOk();
        isResponseOk(body);

    }

    protected void isResponseOk(WebTestClient.ResponseSpec response) {
        response.expectBody(String.class)
            .consumeWith(e -> Assert.assertEquals(200, (e.getStatus().value())));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@SpringBootTest
class UpDownLoadTests {

   //创建webClient
   private WebClient webClient = WebClient.builder()
               .baseUrl("http://localhost:8888/")
               .build();

   @Test
   void testUpload()  {
      // 待上传的文件(存在客户端本地磁盘)
      String filePath = "D:\\data\\local\\splash.png";
      // 封装请求参数
      FileSystemResource resource = new FileSystemResource(new File(filePath));
      MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
      param.add("uploadFile", resource);  //服务端MultipartFile uploadFile
      //param.add("param1", "test");   //服务端如果接受额外参数,可以传递

      // 发送请求
      Mono<String> mono = webClient
                  .post() // POST 请求
                  .uri("/upload")  // 请求路径
                  .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                  .body(BodyInserters.fromMultipartData(param))
                  .retrieve() // 获取响应体
                  .bodyToMono(String.class); //响应数据类型转换

      // 输出结果
      System.out.println(mono.block());
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

文件下载

@SpringBootTest
class UpDownLoadTests {

   //创建webClient
   private WebClient webClient = WebClient.builder()
               .baseUrl("http://localhost:8888/")
               .build();

   @Test
   void testUpload()  {
      // 待上传的文件(存在客户端本地磁盘)
      String filePath = "D:\\data\\local\\splash.png";
      // 封装请求参数
      FileSystemResource resource = new FileSystemResource(new File(filePath));
      MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
      param.add("uploadFile", resource);  //服务端MultipartFile uploadFile
      //param.add("param1", "test");   //服务端如果接受额外参数,可以传递

      // 发送请求
      Mono<String> mono = webClient
                  .post() // POST 请求
                  .uri("/upload")  // 请求路径
                  .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                  .body(BodyInserters.fromMultipartData(param))
                  .retrieve() // 获取响应体
                  .bodyToMono(String.class); //响应数据类型转换

      // 输出结果
      System.out.println(mono.block());
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31