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

一. 关于测试

1. 单元测试的概念

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

2. 集成测试的概念

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

二. Java中的测试

1. 概述

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

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

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

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

2. SpringBoot提供的测试库

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

三. JUnit回顾

1. JUnit简介

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

ps:

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

2. JUnit特性

  1. 测试工具
  2. 测试套件
  3. 测试运行器
  4. 测试分类

3. JUnit测试使用

3.1 添加依赖

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

3.2 创建测试类和测试方法

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

4. JUnit中常用注解

  1. @BeforeClass:针对所有测试,只执行一次,且修饰符必须为static void。
  2. @Before:初始化方法,在执行当前测试类的每个测试方法前执行。
  3. @Test:测试方法,在这里可以测试期望的异常和超时时间。
  4. @After:释放资源,在执行当前测试类的每个测试方法后执行。
  5. @AfterClass:针对所有测试,只执行一次,且必须为static void。
  6. @Ignore:忽略的测试方法(只在测试类的时候生效,单独执行该测试方法无效)。
  7. @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 能够被扫描得到,它们分别是:

  1. @Controller
  2. @ControllerAdvice
  3. @JsonComponent
  4. Filter
  5. WebMvcConfigurer
  6. 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 提供了两种测试方式:

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

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,这就是设置桩数据的预期数据。

转自:知乎一一哥

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270