本篇来学习开发者工具和单元测试相关的知识,对于一个开发者来说了解开发工具和单元测试对于自身水平提高有着非常大的帮助。SpringBoot中提供了一组开发者工具spring-boot-devtools,用于提升开发效率,开发人员可以将该模块包含在任何项目中,而spring-boot-devtools最令人喜欢的地方就是支持热部署,当开发者修改了部分代码或者资源后,SpringBoot项目会自动更新。

devtools使用

devtools基本使用

当开发者想要在项目中加入devtools模块,只需添加相关依赖即可:

1
2
3
4
5
6
<!--引入devtools依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

注意这里多了一个optional选项,目的是为了防止将devtools依赖传递到其他模块中。当开发者将应用打包运行后,devtools会被自动禁用。

当开发者将spring-boot-devtools引入项目后,只要classpath路径下的文件发生了变化,那么项目就会自动重启,这无疑是极大地提升了项目的开发效率。如果开发者使用的是Eclipse开发工具,那么在修改完代码并保存之后, 项目将自动编译并触发重启,而开发者如果使用了IntelliJ IDEA开发工具,默认情况下, 需要开发者手动编译才会触发重启。手动编译时,单击BuildBuild Project菜单或者按Ctrl+F9快捷键进行编译,编译成功后就会触发项目重启。当然,使用IntelliJ IDEA的开发者也可以配置项目自动编译,配置步骤如下:

【第一步,打开自动编译】单击File→单击Settings菜单→单击Build,Execution,Deployment→Compile→勾选Build project automatically→点击Apply→点击Save,如下图所示:

【第二步,打开运行时编译】按Ctrl+Alt+Shift+/快捷键(注意这是针对默认的IDEA快捷键方式)调出Maintenance页面,然后选择Register页面,如下图所示:

在新打开的Register页面中选择compiler.automake.allow.when.app.running选项即可:

做完这两步设置后,若开发者再次在IDEA中修改代码,则项目会自动重启。不过需要注意的是classpath路径下的静态资源或者视图模板等发生变化时,并不会导致项目重启

基本原理

SpringBoot中使用的自动重启技术涉及到两个类加载器,一个是baseclassloader,用来加载不会变化的类,如项目中引用的第三方jar包等;另一个是restartclassloader,用来加载开发者自己写的会变化的类。当项目需要重启时,restartclassloader将被一个新创建的类加载器代替,而baseclassloader继续使用原来的,这样启动方式比冷启动快很多,因为baseclassloader已经存在且已经加载完成。

自定义监控资源

默认情况下,/META_INF/maven/META_INF/resources/resources/static/public以及/templates位置下的资源的变化并不会触发重启,如果开发者想对这些位置进行重定义,只需要在application.properties配置文件中添加如下配置即可:

1
2
# 设置static目录资源修改触发项目重新启动
spring.devtools.restart.exclude=static/**

这表示从默认的不触发重启目录中除去static目录,即当classpath:/static目录下的资源发生变化时也会导致项目重启。当然开发者也可以反向配置需要监控的目录,配置方式为:

1
spring.devtools.restart.additional-paths=src/main/resources/static

这个配置表示当/src/main/resources/static目录下的文件发生变化时,会自动重启项目。

由于项目的编码过程是一个连续的,并不是每修改一行代码就需要重启项目,这样不仅浪费电脑的性能,而且没有实际意义。鉴于这种情况,开发者也可以考虑使用触发文件的方式,触发文件是一个特殊的文件,当这个文件发生变化时项目就会重启,配置方式如下:

1
spring.devtools.restart.trigger-file=.trigger-file

在项目resources目录下创建一个名为.trigger-file的文件,此时开发者修改代码时,默认情况下项目不会重启,需要项目重启时,开发者只需要修改.trigger-file文件即可。但是注意如果项目没有改变,只是单纯地改变了.trigger-file文件,那么项目也不会重启。

使用LiveReload

前面介绍了静态资源目录下的文件变化以及模板文件的变化不会引发重启,虽然开发者可以通过修改配置改变这一默认情况,但实际上并没有必要,因为静态资源不是class。devtools默认嵌入了LiveReload服务器,可以解决静态文件的热部署问题,LiveReload可以在资源发生变化时自动触发浏览器更新,LiveReload支持Chrome、Firefox以及Safari。Chrome用户只需要在应用商店搜索LiveReload并下载即可:

下载并添加完毕后,Chrome浏览器的右上角就有一个LiveReload图标,如下图所示:

在浏览器中打开项目的页面,然后点击浏览器右上角的LiveReload按钮,开启LiveReload连接,此时当静态资源发生改变时,浏览器就会自动加载。如果开发者不想使用这一特性,可以通过在application.properties配置文件中添加如下配置进行关闭:

1
2
# 关闭LiveReload功能
spring.devtools.livereload.enabled=false

请注意,建议开发者使用LiveReload策略,而不是重启策略来实现静态资源的动态加载,因为项目重启所消耗的时间一般都要超过LiveReload。

禁用自动重启

如果开发者在项目中添加了spring-boot-devtools依赖,但是不想使用自动重启特性,那么可以关闭自动重启功能,只需要在application.properties配置文件中添加如下配置:

1
2
# 关闭restart自动重启功能
spring.devtools.restart.enabled=false

当然也可以在项目的启动类上添加代码,来禁止自动重启:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
public class MongodbrestfulspringbootApplication {

public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled","false");
SpringApplication.run(MongodbrestfulspringbootApplication.class, args);
}

}

全局配置

如果项目模块众多,开发者可以在当前用户目录下创建.spring-boot-devtools.properties文件,以对devtools进行全局配置,这个配置文件适用于当前计算机上任何使用了devtools模块的SpringBoot项目。以笔者的电脑为例,在C:\Users\Administrator目录新建一个.spring-boot-devtools.properties`文件,其中的代码为:

1
spring.devtools.restart.trigger-file=.trigger-file

这样就实现了使用触发文件的方式来重启项目,这是对这台计算机上所有的SpringBoot项目都适用的配置。

单元测试

终于到了介绍单元测试的时候了,之前写的代码都是新建一个Controller类进而来进行测试,毫无疑问这种操作是非常臃肿的,效率非常低下,在SpringBoot中使用单元测试可以实现对每一个环节的代码进行测试。SpringBoot中的单元测试继承自Spring但是又做了大量的简化,开发者只需要更少量的代码就能搭建一个测试环境,进而实现对Dao、Service和Controller层的代码进行测试。

基本用法

其实细心的你可能已经发现,新创建的SpringBoot Web项目中都已经默认添加了spring-boot-starter-test依赖,并且创建好了测试类。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为testspringboot,接着打开com.envy.testspringboot.TestspringbootApplicationTests测试文件,可以发现其中的代码为:

1
2
3
4
5
6
7
8
9
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestspringbootApplicationTests {

@Test
public void contextLoads() {
}

}

请注意笔者使用的SpringBoot版本是2.1.14,似乎2.2的版本测试已经发生变化了,这个后续会出文章进行介绍。

解释一下上述代码中的含义:

  • @RunWith(SpringRunner.class)注解的含义为将JUnit执行类修改为SpringRunner,而SpringRunner是Spring Framework中测试类SpringJUnit4ClassRunner的别名。
  • @SpringBootTest注解除了提供Spring TestContext中的常规测试功能外,还提供了其他特性:提供默认的ContextLoader、自动搜索@SpringBootConfiguration、自定义环境属性、为不同的WebEnvironment模式提供支持,这里的WebEnvironment模式主要有4种方式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static enum WebEnvironment {
    MOCK(false),
    RANDOM_PORT(true),
    DEFINED_PORT(true),
    NONE(false);

    private final boolean embedded;

    private WebEnvironment(boolean embedded) {
    this.embedded = embedded;
    }

    public boolean isEmbedded() {
    return this.embedded;
    }
    }
    (1)MOCK,这种模式是当classpath下存在servletAPIS时,就会创建WebApplicationContext并提供一个mockservlet环境;当classpath下存在Spring WebFlux时,就会创建ReactiveWebApplicationContext;若都不存在则创建一个常规的ApplicationContext
    (2)RANDOM_PORT,这种模式将提供一个真实的Servlet环境,使用内嵌的容器,但是端口随机。
    (3)DEFINED_PORT,这种模式也将提供一个真实的Servlet环境,使用内嵌的容器,但是使用的是定义好的端口。
    (4)NONE,这种模式则加载一个普通的ApplicationContext,不提供任何Servlet环境。这种一般不适用于Web测试。
  • 在Spring测试中,开发者一般使用@ContextConfiguration(class =)或者@ContextConfiguration(locations=)来指定要加载的Spring配置,而在SpringBoot中则不需要这么麻烦,SpirngBoot中的@*Test注解会去包含测试类的包下查找带有@SpringBootApplication或者@SpringBootConfiguration注解的主配置类。
  • @Test注解则来自Junit,Junit中的@After@AfterClass@Before@BeforeClass@Ignore等注解一样可以在SpringBoot中使用。

接下来就简单学习一下如何进行Service、Controller和JSON等测试。

Service测试

Service层测试就是通常意义上的测试,非常简单,如这里在service包内新建了一个HelloService类,里面的代码为:

1
2
3
4
5
6
@Service
public class HelloService {
public String hello(String name){
return "hello,"+name;
}
}

接下来需要对HelloService进行测试,非常简单可以使用IDEA自带的功能:

然后在测试方法上添加@RunWith(SpringRunner.class)@SpringBootTest注解,并使用@Autowried注解来导入需要测试的类,结合断言即可实现测试,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloServiceTest {

@Autowired
private HelloService helloService;

@Test
public void hello() {
String result = helloService.hello("envy");
Assert.assertNotNull(result);
}
}

这里使用Assert断言仅仅是根据返回的result结果是否为null来进行判断,开发者可根据实际情况自行设置。

Controller测试

请注意Controller的测试通常也称为API测试,它的测试需要借助于Mock测试,即对一些不易获取的对象采用虚拟的对象来创建进而便于测试。Spring中提供的MockMvc则提供了对Http请求的模拟,使开发者能够在不依赖网络环境的情况下实现对Controller的快速测试。接下来举一个比较完整的controller测试例子,来详细介绍controller测试的相关流程。

第一步,导入lombok依赖,新建pojo包并创建Book实体类,其中的代码为:

1
2
3
4
5
6
@Data
public class Book {
private Integer id;
private String name;
private String author;
}

第二步,新建controller包并创建BookController类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class BookController {

@GetMapping("/hello")
public String hello(String name){
return "hello,"+name;
}

@PostMapping("/addBook")
public String addBook(@RequestBody Book book){
return book.toString();
}
}

第三步,使用IDEA自带的GOTO来生成测试文件,此时就需要借助于MockMvc,相应的代码为:

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
@RunWith(SpringRunner.class)
@SpringBootTest
public class BookControllerTest {
@Autowired
private HelloService helloService;
@Autowired
private WebApplicationContext webApplicationContext;

MockMvc mockMvc;

@Before
public void before(){
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

@Test
public void hello() throws Exception {
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.get("/hello")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("name","envy"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}

@Test
public void addBook() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
Book book = new Book();
book.setName("西游记");
book.setAuthor("吴承恩");
book.setId(1);
String s = objectMapper.writeValueAsString(book);
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/addBook")
.contentType(MediaType.APPLICATION_JSON)
.content(s))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());

}
}

简单解释一下上述代码的含义:首先下面这段代码用于注入一个WebApplicationContext 对象,用于模拟ServletContext环境:

1
2
@Autowired
private WebApplicationContext webApplicationContext;

接着使用MockMvc mockMvc;来声明一个MockMvc对象,并在每个测试方法执行前执行MockMvc的初始化操作(这里使用了@Before注解)

1
2
3
4
@Before
public void before(){
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

然后调用MockMvc中的perform方法开启一个RequestBuilder请求,具体的请求则是通过MockMvcRequestBuilders进行构建,调用MockMvcRequestBuilders中的get方法表示发起一个GET请求,调用post表示发起一个POST请求,其他的DELETE和PUT请求也是一样的,最后通过调用param方法来设置请求参数。

.andExpect(MockMvcResultMatchers.status().isOk())表示添加返回值的验证规则,可以使用MockMvcResultMatchers进行验证,这里表示验证相应码是否为200。.andDo(MockMvcResultHandlers.print())表示将请求详细信息打印到控制台。.andReturn();表示返回相应的MvcResult对象,且使用System.out.println(mvcResult.getResponse().getContentAsString());将返回的信息以字符串的方式进行返回和输出。

addBook方法则演示了POST请求如何传递JSON数据,首先将一个book对象转为一段JSON,然后设置请求的contentType为APPLICATION_JSON,最后设置content为上传的JSON即可。

TestRestTemplate

除了MockMvc这种测试方法之外,SpringBoot还专门提供了TestRestTemplate用来实现集成测试,若开发者使用了@SpringBootTest注解,则TestRestTemplate将自动可用,直接在测试类中注入即可。注意,如果使用TestRestTemplate进行测试,则需要将@SpringBootTest注解中WebEnvironment默认的MOCK修改为RANDOM_PORT或者是DEFINED_PORT,因为这两种都是使用一个真实的Servlet环境而不是模拟的Servlet环境。此时的测试代码为:

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)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT )
public class BookControllerTestTwo {
@Autowired
private TestRestTemplate testRestTemplate;

@Test
public void hello(){
ResponseEntity<String> hello = testRestTemplate.getForEntity("/hello?name={0}",String.class,"envy");
System.out.println(hello.getBody());
}

@Test
public void addBook(){
Book book = new Book();
book.setName("三国演义");
book.setAuthor("罗贯中");
book.setId(2);
ResponseEntity<String> result = testRestTemplate.postForEntity("/addBook",book,String.class);
System.out.println(result.getBody());
}
}

注意这里只是简单的介绍了TestRestTemplate的用法,后续会有文章深入介绍TestRestTemplate的其他用法。

JSON测试

开发者可以使用@JsonTest注解来测试JOSN序列化和反序列化是否正常运行,该注解将自动配置Jackson ObjectMapper@Json Component、以及Json Modules。注意开发者也可以使用Gson来代替Jackson,那么该注解将配置Gson。请注意既然是测试JSON,因此不能在测试类上添加@SpringBootTest注解,因为它用于测试Web项目。举个例子来说,新建一个JSONTest测试类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(SpringRunner.class)
@JsonTest
public class JSONTest {
@Autowired
JacksonTester<Book> jacksonTester;

@Test
public void testSerialize() throws Exception {
Book book = new Book();
book.setName("西游记");
book.setAuthor("吴承恩");
book.setId(1);
System.out.println(jacksonTester.write(book).getJson());
Assertions.assertThat(jacksonTester.write(book)).hasJsonPathStringValue("@.name");
Assertions.assertThat(jacksonTester.write(book)).extractingJsonPathStringValue("@.name").isEqualTo("西游记");
}

@Test
public void testDeserialize() throws Exception {
String content = "{\"id\":1,\"name\":\"西游记\",\"author\":\"吴承恩\"}";
Assertions.assertThat(jacksonTester.parseObject(content).getName()).isEqualTo("西游记");
}
}

简单解释一下上述代码的含义:

  • 首先添加JsonTest注解,然后注入JacksonTester进行JSON的序列化和反序列化测试。testSerialize方法是序列化操作,用于将Java对象序列化为JSON对象。jacksonTester.write(book).getJson()用于将序列化的结果以JSON格式进行显示。Assertions.assertThat(jacksonTester.write(book)).hasJsonPathStringValue("@.name");用于判断在对象序列化完成后生成的JSON中是否有一个名为name的key。再下面的代码用于判断序列化后name对应的值是否为”西游记”。
  • testDeserialize方法是反序列化操作,用于将JSON对象反序列化为Java对象,然后完成时判断对象的name属性是否为”西游记”:

开发者工具与单元测试小结

本篇学习了SpringBoot中的开发者工具和单元测试,开发者工具的一个核心功能就是热部署,结合LiveReload可以极大地缩短开发者等待编译的时间,有效提高开发效率。单元测试则与Spring单元测试一脉相承,但是又增加了许多功能,同时简化了测试代码,是开发者极大地节省了测试的编码时间。注意这里仅仅只是介绍了一些常用的功能,后续会出一些文章来介绍SpringBoot官方文档中关于单元测试那一部分的知识。养成一个良好的代码测试习惯对于提高个人编程水平具有深刻的影响。