SpringBoot2.x系列教程76--Java测试详解

一. 关于测试

1. 单元测试的概念

在计算机编程中,单元测试是一种软件测试方法,用以测试源代码的单个单元、一个或多个计算机程序模块的集合以及相关的控制数据、使用过程和操作过程,以确定它们是否适合使用。
通俗的说,我们在做单元测试时,只是测试了一个代码单元,也就是每次只测试一个方法,不包括与正测试组件相交互的其他所有代码组件。

2. 集成测试的概念

集成测试(有时也称集成和测试,缩写为 I&T)是软件测试的一个阶段,在这个阶段中,各个软件模块被组合在一起来进行测试。
通俗的说,我们在集成测试中是把各组件进行集成在一起测试。

二. Java中的测试

1. 概述

在一般的Java开发中,我们主要是使用JUnit进行测试功能的实现。

而在Spring Boot中,则提供了很多有用的工具类和注解,用于帮助我们测试自己的应用,主要分两个模块:

  • spring-boot-test:包含核心组件;
  • spring-boot-test-autoconfigure:为测试提供自动配置。

但是我们在利用SpringBoot进行开发的时候,一般只需要引用spring-boot-starter-test-starter依赖包就可以了,它涵盖了以上两大模块,既为我们提供了Spring Boot测试模块的依赖,也提供了JUnit,AssertJ,Hamcrest等很多有用的依赖。

2. SpringBoot提供的测试库

  • JUnit - 事实上的(de-facto)标准,用于Java应用的单元测试;
  • Spring Test & Spring Boot Test  - 对Spring应用的集成测试支持;
  • AssertJ - 一个流式断言库;
  • Hamcrest - 一个匹配对象的库(也称为约束或前置条件);
  • Mockito - 一个Java模拟框架;
  • JSONassert - 一个针对JSON的断言库;
  • JsonPath - 用于JSON的XPath.

三. JUnit回顾

1. JUnit简介

JUnit 是一个回归测试框架,经常被Java开发者用于实施对应用程序的单元测试,加快程序编制速度,同时提高编码的质量。

ps:

回归测试是指修改了旧代码后,重新进行测试以确认本次修改没有引入新的错误或导致其他代码产生错误,也就是要重复以前的全部或部分相同测试。

2. JUnit特性

  • 测试工具
  • 测试套件
  • 测试运行器
  • 测试分类

3. JUnit测试使用

3.1 添加依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
    <version>4.12</version>
</dependency>

3.2 创建测试类和测试方法

  • 测试类的命名规则一般是 xxxTest.java;
  • 测试类中的测试方法一般都有前缀,比如在测试方法上带有test前缀;
  • 在测试方法上添加@Test注解。

4. JUnit中常用注解

  • @BeforeClass:针对所有测试,只执行一次,且修饰符必须为static void。
  • @Before:初始化方法,在执行当前测试类的每个测试方法前执行。
  • @Test:测试方法,在这里可以测试期望的异常和超时时间。
  • @After:释放资源,在执行当前测试类的每个测试方法后执行。
  • @AfterClass:针对所有测试,只执行一次,且必须为static void。
  • @Ignore:忽略的测试方法(只在测试类的时候生效,单独执行该测试方法无效)。
  • @RunWith: 更改测试运行器,缺省值org.junit.runner.Runner

5. 单元测试类执行顺序

@BeforeClass –> @Before –> @Test –> @After –> @AfterClass

6. 测试方法的调用顺序

@Before –> @Test –> @After

7. JUnit中的异常测试

我们可以利用@Test注解和expected 参数,来测试我们的代码中是否会抛出某个可能的异常。

@Test(expected = NullPointerException.class)
public void testNullException() {
    throw new NullPointerException();
}

8. JUnit中的超时测试

我们可以利用@Test注解和timeout参数,来测试我们的代码是否比指定的毫秒数花费了更多的时间。

@Test(timeout = 1000)
public void testTimeout() throws InterruptedException {       
    TimeUnit.SECONDS.sleep(2);
    System.out.println("Success");
}

9. JUnit中的套件测试

我们可以利用@Suite.SuiteClasses注解,将多个测试类整合在一起,形成一个测试套件进行测试。

public class TaskOneTest {
    @Test
    public void test() {
        System.out.println("任务一...");
    }
}

public class TaskTwoTest {
    @Test
    public void test() {
        System.out.println("任务二...");
    }
}

public class TaskThreeTest {
    @Test
    public void test() {
        System.out.println("任务三...");
    }
}

// 1. 更改测试运行方式为 Suite
@RunWith(Suite.class) 
// 2. 将测试类传入进来
@Suite.SuiteClasses({TaskOneTest.class, TaskTwoTest.class, TaskThreeTest.class})
public class SuitTest {
    /**
     * 测试套件的入口类只是组织测试类一起进行测试,无任何特别的测试方法。
     */
}

10. JUnit中的参数化测试

从Junit 4开始,引入了一个新的参数化测试功能。参数化测试允许开发人员使用不同的值反复运行同一个测试方法。

我们可以遵循以下 5个步骤来创建参数化测试方法。

  1. 用 @RunWith(Parameterized.class)来注解 test 类;
  2. 创建一个由 @Parameters 注解的公共静态方法,返回一个对象的集合(数组)来作为测试参数的数据集合;
  3. 创建一个公共的构造函数,接收和测试数据相等的内容;
  4. 为每一个测试数据创建一个实例变量;
  5. 用实例变量作为测试数据的来源来创建你的测试用例。
//1.更改默认的测试运行器为RunWith(Parameterized.class)
@RunWith(Parameterized.class)
public class ParameterTest {

    // 2.声明存放预期值和测试数据的变量
    private String firstName;
    private String lastName;

    //3.声明一个返回值为Collection的公共静态方法,并使用@Parameters注解进行修饰
@Parameterized.Parameters
    public static List<Object[]> param() {
        //这里给出了两个测试用例
        return Arrays.asList(new Object[][]{{"Mike", "Black"}, {"Cilcln", "Smith"}});
    }

    //4.为测试类声明一个带有参数的公共构造函数,并在其中为之声明变量赋值
    public ParameterTest(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    // 5. 进行测试,发现它会将所有的测试用例测试一遍
    @Test
    public void test() {
        String name = firstName + " " + lastName;
        assertThat("Mike Black", is(name));
    }
}

11. JUnit中使用assertThat断言

JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat。

#语法
assertThat( [actual], [matcher expected] );

assertThat 使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配规则,来精确的指定一些想要设定满足的条件,具有很强的易读性,而且使用起来更加灵活。

四. SpringBoot中的测试功能

1. SpringBoot中的测试依赖包

Spring 框架提供了一个专门的测试模块(spring-test),用于应用程序的集成测试。而在 Spring Boot 中,我们可以通过spring-boot-starter-test启动器快速开启和使用它。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

2. 创建测试类

// 获取SpringBoot启动类,加载配置,确定装载 Spring 程序的装载方法,它会去寻找 主配置启动类(也就是被 @SpringBootApplication 注解的类)
@SpringBootTest
// 让 JUnit 运行 Spring 的测试环境,获得 Spring 上下文环境的支持。
@RunWith(SpringRunner.class)
public class OrderTest {
    // ...
}

3. SpringBoot中实现测试的方式

在SpringBoot中实现测试功能,我们可以利用以下4种方式进行相关的测试实现。

  1. @WebMvcTest注解:针对单个的Spring MVC控制器实现单元测试,该方式不需要完整启动 HTTP 服务器就可以快速测试 MVC 控制器;
  2. @SpringBootTest注解:该方式可以启动一个完整的 HTTP 服务器,对整个Spring Boot 的 Web 应用编写测试代码。
  3. @DataJpaTest注解:使用 @DataJpaTest注解表示只对 JPA 进行测试。
  4. Mockito方式:对于一些不容易构造的、或者和这次单元测试无关但是上下文又有依赖的对象,用一个虚拟的对象(Mock对象)来模拟,以便单元测试能够进行。

4. @WebMvcTest注解对单个Controller进行单元测试

如果我们想对 Spring MVC 控制器编写单元测试代码时,可以使用@WebMvcTest注解。它提供了自配置的 MockMvc,可以不需要完整启动 HTTP 服务器,就能够快速测试 MVC 控制器。

4.1 构建一个测试用的Controller

@RestController
@RequestMapping(value = "/order", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping
    public ResponseEntity<List<OrderResult>> listAll() {
        return ResponseEntity.ok(orderService.findOrders());
    }
}

4.2 编写 MockMvc 的测试类

@RunWith(SpringRunner.class)
@WebMvcTest(OrderController.class)
public class OrderControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private OrderService orderService;

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

    @Test
    public void listAll() throws Exception {
        mvc.perform(MockMvcRequestBuilders
            .get("/order"))
            .andExpect(status().isOk()) // 期待返回状态吗码200
            // JsonPath expression  https://github.com/jayway/JsonPath
            //.andExpect(jsonPath("$[1].name").exists()) // 这里是期待返回值是数组,并且第二个值的 name 存在,所以这里测试是失败的
            .andDo(print()); // 打印返回的 http response 信息
    }
}

注意

在我们使用@WebMvcTest注解时,只有部分Bean 能够被扫描得到,它们分别是:

  • @Controller
  • @ControllerAdvice
  • @JsonComponent
  • Filter
  • WebMvcConfigurer
  • HandlerMethodArgumentResolver

其他常规的@Component(包括@Service、@Repository等)Bean 则不会被加载到 Spring 的测试上下文环境中。

4.3 注入Spring上下文环境到 MockMvc中

可以如下编写 MockMvc 的测试类:

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderControllerTest {

    /**
     * Interface to provide configuration for a web application.
     */
    @Autowired
    private WebApplicationContext ctx;

    private MockMvc mockMvc;

    /**
     * 初始化 MVC 的环境
     */
    @Before
    public void before() {
        //使用 WebApplicationContext构建 MockMvc
        mockMvc = MockMvcBuilders.webAppContextSetup(ctx).build();
    }

    @Test
    public void listAll() throws Exception {
        mockMvc.perform(get("/order") // 测试的相对地址
                .accept(MediaType.APPLICATION_JSON_UTF8))// accept response content type
                .andExpect(status().isOk()) // 期待返回状态吗码200
                // JsonPath expression  https://github.com/jayway/JsonPath
                .andExpect(jsonPath("$[1].name").exists()) // 这里是期待返回值是数组,并且第二个值的 name 存在
                .andDo(print()); // 打印返回的 http response 信息
    }
}

5. @SpringBootTest测试完整Web应用

当我们想启动一个完整的 HTTP 服务器,对 Spring Boot 的 Web 应用编写测试代码时,可以使用@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)注解开启一个随机的可用端口。

Spring Boot 针对 REST 调用的测试,提供了一个 TestRestTemplate 模板,它可以解析链接服务器的相对地址。

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

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void listAll() {
        // 由于这里返回的是 List 类型数据,可以使用 exchange 函数进行类型转换。
        ParameterizedTypeReference<List<OrderResult>> type = new ParameterizedTypeReference<List<OrderResult>>() {};
        ResponseEntity<List<OrderResult>> result = restTemplate.exchange("/order", HttpMethod.GET, null, type);
        Assert.assertThat(result.getBody().get(0).getName(), Matchers.notNullValue());
    }
}

6. @DataJpaTest测试JPA

我们可以使用 @DataJpaTest注解对 JPA 进行测试,其中@DataJpaTest注解只会扫描@EntityBean和装配了Spring Data JPA 的存储库,其他常规的@Component(包括@Service、@Repository等)Bean 则不会被加载到 Spring 的上下文测试环境中。

@DataJpaTest 提供了两种测试方式:

  • 使用内存数据库 h2Database,Spring Data Jpa 测试默认采取的就是这种方式;
  • 使用真实环境的数据库。

6.1 使用内存数据库测试

默认情况下,@DataJpaTest使用的是内存数据库进行测试,我们无需配置和启用真实的数据库,只需要在 pom.xml 配置文件中声明如下依赖即可:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

编写测试方法:

@RunWith(SpringRunner.class)
@DataJpaTest
public class OrderDaoTest {

    @Autowired
    private OrderDao orderDao;

    @Test
    public void testSave() {
        Order order = new Order();
        OrderDetail detail = new OrderDetail();
        detail.setName("tv");
        order.setDetail(detail);
        assertThat(detail.getName(), Matchers.is(orderDao.save(order).getDetail().getName()));
    }
}

6.2 使用真实数据库测试

如要需要使用真实环境中的数据库进行测试,则需要替换掉默认的规则,使用@AutoConfigureTestDatabase(replace = Replace.NONE)注解。

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class OrderDaoTest {

    @Autowired
    private OrderDao orderDao;

    @Test
    public void testSave() {
        Order order = new Order();
        OrderDetail detail = new OrderDetail();
        detail.setName("tv");
        order.setDetail(detail);
        assertThat(detail.getName(), Matchers.is(orderDao.save(order).getDetail().getName()));
    }
}

6.3 事务控制

当我们执行上面新增数据的测试时,可能会发现测试正常通过,但是数据库却并没有新增数据。原因是默认情况下,在每个 JPA 测试结束时,事务会发生回滚,这样可以在一定程度上防止测试数据污染数据库。
但是如果我们不希望事务发生回滚,可以使用@Rollback(false)注解,该注解可以标注在类级别做全局的控制,也可以标注在某个特定的不需要执行事务回滚的方法上。
另外我们也可以显式的使用 @Transactional注解,设置事务和事务的控制级别,放大事务的范围。

7. Mockito模拟对象

JUnit和SpringTest基本上可以满足绝大多数的单元测试了,但是由于现在的系统越来越复杂,相互之间的依赖越来越多,特别是微服务化以后的系统,往往一个模块的代码需要依赖几个其他模块的东西。

因此,我们在做单元测试的时候,往往很难构造出需要的依赖。一个单元测试,我们只关心一个小的功能,但是为了这个小的功能能跑起来,可能需要依赖一堆其他的东西,这就导致了单元测试无法进行。所以,我们就需要再测试过程中引入Mock测试。

所谓的Mock测试就是在测试过程中,对于一些不容易构造的、或者和这次单元测试无关但是上下文又有依赖的对象,用一个虚拟的对象(Mock对象)来模拟,以便单元测试能够进行。

比如有一段代码的依赖为:

当我们要进行单元测试的时候,就需要给A注入B和C,但是C又依赖了D,D又依赖了E。这就导致了,A的单元测试很难得进行。

但是,当我们使用了Mock来进行模拟对象后,我们就可以把这种依赖解耦,只关心A本身的测试,它所依赖的B和C,全部使用Mock出来的对象,并且给MockB和MockC指定一个明确的行为。

如下图所示:

因此,当我们使用Mock后,对于那些难以构建的对象,可以使用模拟对象,只需要提前做桩数据Stubbing即可。

所谓做桩数据,也就是告诉Mock对象,当与之交互时执行何种行为的过程。比如当调用B对象的b()方法时,我们期望返回一个true,这就是设置桩数据的预期数据。

展开阅读全文
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值