- devtools 简介
- devtools 实战
- 单元测试
Spring Boot 中提供了一组开发工具 spring-boot-devtools ,可以提高开发者的工作效率,开发者可以将该模块包含在任何项目中,spring-boot-devtools 最方便的地方莫过于热部署了。
8.2 devtools 实战想要在项目中加入 devtools 模块,只需要添加相关依赖即可
注意:这里多了一个 optional 选项,是为了防止将 devtools 依赖传递到其它模块中。当开发者将应用打包运行后,devtools 会自动被禁用。
org.springframework.boot spring-boot-devtools true
当开发者将 spring-boot-devtools 引入项目后,只要 classpath 路径下的文件发生了变化,项目就会自动重启,这极大的提高了项目的开发速度。
如果开发者使用了 eclipse ,那么在修改完代码并保存后,项目将自动编译并处罚重启,如果使用了 IDEA ,默认情况下,需要开发者手动编译才会触发重启。手动编译时,单击 Build->Build Project 菜单或者按 Ctrl + F9 快捷键进行编译,编译成功后就会触发项目重启。当然,使用 IDEA 也可以配置项目自动编译,步骤如下:
步骤01:单击 File->Settings 菜单,打开 Setting 页面,在左边的菜单栏一次找到 Build,Execution,Deployment->Compile,勾选 Build project automatically,如图:
步骤02:按 Ctrl+Shift+Alt+/ 快捷键调出 Maintenance 页面,如图
单击 Registry ,在新打开的 Registry 页面中,勾选以下复选框
做完这两步配置后,若开发者在此在 IDEA 中修改代码,则项目会自动重启。
注意:classpath 路径下的静态资源或视图模板等发生变化时,并不会导致项目重启。
Spring Boot 中使用的自动重启技术涉及两个类加载器,一个是 baseclassloader ,用来加载不会变化的类,例如项目引用的第三方的 jar ;另一个是 restartclassloader,用来加载开发者自己写的会变化的类。
当项目需要重启时,restartclassloader 将被一个新创建的类加载器代替,而 baseclassloader 则继续使用原来的,这种启动方式要比冷启动快得多,因为 baseclassloader 已经存在并且已经加载好了。
默认情况下,/META-INF/maven、/META-INF/resources、/resources、/static、/public 以及 /templates位置下资源的变化并不会触发重启,如果想要对这些位置进行重定义,在application.properties中添加如下配置即可:
spring.devtools.restart.exclude=static/**
这表示从默认不触发重启的目录中除去 static 目录,即 classpath:static目录下的资源发生变化时也会导致项目重启。用户也可以反向配置需要监控的目录,配置方式如下:
spring.devtools.restart.additional-paths=src/main/resources/static
这个配置表示当src/main/resources/static 目录下的文件发生变化时,自动重启项目。
由于项目的编码过程是一个连续的过程,并不是每修改一行代码就要重启项目,这样不仅浪费电脑性能,而且没有实际意义。鉴于这种情况,开发者也可以考虑使用触发文件,触发文件是一个特殊的文件,当这个文件发生变化时项目就会重启,配置方法如下:
spring.devtools.restart.trigger-file=.trigger-file
在项目 resources 目录下创建一个名为 .trigger-file 的文件,此时开发者修改代码时,默认情况下项目不会重启,需要项目重启时,开发者只需要修改 .trigger-file 文件即可。但是注意,如果项目没有改变,只是单纯地改变了 .trigger-file 文件,那么项目不会重启。
8.2.4 使用 LiveReloaddevtools 默认嵌入了 LiveReload 服务器,可以解决静态文件的热部署,LiveReload 可以在资源发生变化时自动触发浏览器更新,LiveReload 支持 Chrome、Firefox、Safari。以 Chrome 为例,在 Chrome 应用商店搜索 LiveReload ,如图
没搜到,本小结 略
8.2.5 禁用自动重启如果开发者添加了 spring-boot-devtools 依赖但是不想使用自动重启特性,那么可以关闭自动重启
spring.devtools.restart.enabled=false8.2.6 全局配置
如果项目模块众多,开发者可以在当前用户目录下创建.spring-boot-devtools 文件来对 devtools 进行全局配置,这个配置文件适用于当前计算机上任何使用了 devtools 模块的 Spring Boot 项目。例:E:Giteechapter04srcmainresources 目录下创建.spring-boot-devtools.properties 文件,内容如下:
spring.devtools.restart.trigger-file=.trigger-file
此时就实现了使用触发文件触发项目重启。
8.3 单元测试 8.3.1 基本用法使用 IDEA 创建一个 Spring Boot 项目时,创建成功后,默认都会添加 spring-boot-starter-test 依赖,并且创建好了测试类,代码如下:
@RunWith(SpringRunner.class) @SpringBootTest class Test01ApplicationTests { @Test void contextLoads() { } }
代码解释:
- 首先使用了 @RunWith 注解,该注解将 JUnit 执行类修改为 SpringRunner,而SpringRunner 是 Spring Framework 中测试类 SpringJUnit4ClassRunner 的别名。
- @SpringBootTest 注解除了提供 Spring TestContext 中的常规测试功能之外,还提供了其它特性:提供默认的 ContextLoader、自动搜索 @Spring BootConfiguration、自定义环境属性、为不同的 webEnvionment 模式提供支持,这里的 webEnvironment 模式主要有四种:
(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 配置,而在 Spring Boot 中则不需要这么麻烦,Spring Boot 中的 @*Test 注解将会去包含测试类的包下查找带有 @SpringBootApplication 或者 @Spring BootConfiguration 注解的主配置类
- @Test 注解则来自 junit,Junit 中的 @After 、 @AfterClass、@Before、@BeforeClass、@Ignore 等注解一样可以在这里使用
Service 层的测试就是常规测试,例如有个 HelloService 如下:
@Service public class HelloService { public String sayHello(String name) { return "Hello " + name + " !"; } }
需要对 HelloService 进行测试,直接在测试类中注入 HelloService 即可:
@RunWith(SpringRunner.class) @SpringBootTest public class Test01ApplicationTests { @Autowired HelloService helloService; @Test public void contextLoads(){ String hello = helloService.sayHello("唐三"); Assert.assertThat(hello, Matchers.is("Hello 唐三 !")); } }
在测试类中注入 HelloService ,然后调用相关方法即可。使用 Assert 判断测试结果是否正确。
8.3.3 Controller 测试Controller 测试则要使用到 Mock 测试,即对一些不易获取的对象采用虚拟的对象来创建进而方便测试。而 Spring 中提供的 MockMvc 则提供了对 HTTP 请求的模拟,使开发者能够在不依赖网络环境的情况下对 Controller 的快速测试。
@RestController public class HelloController { @Autowired HelloService helloService; @GetMapping("/hello") public String hello(String name) { return helloService.sayHello(name); } @PostMapping("/book") public String addBook(@RequestBody Book book) { return book.toString(); } }
public class Book { private Integer id; private String name; private String author; @Override public String toString() { return "Book{" + "id=" + id + ", name='" + name + ''' + ", author='" + author + ''' + '}'; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } }
如果要对这个 Controller 进行测试,就需要借助 MockMvc,如下
@RunWith(SpringRunner.class) @SpringBootTest public class Test01ApplicationTests { @Autowired WebApplicationContext wac; MockMvc mockMvc; @Before public void before() { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } @Test public void test1() throws Exception { MvcResult mvcResult = mockMvc.perform( MockMvcRequestBuilders .get("/hello") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("name", "Michael")) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()) .andReturn(); System.out.println(mvcResult.getResponse().getContentAsString()); } @Test public void test2() throws Exception { ObjectMapper om = new ObjectMapper(); Book book = new Book(); book.setAuthor("罗贯中"); book.setName("三国演义"); book.setId(1); String s = om.writeValueAsString(book); MvcResult mvcResult = mockMvc .perform(MockMvcRequestBuilders .post("/book") .contentType(MediaType.APPLICATION_JSON) .content(s)) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); System.out.println(mvcResult.getResponse().getContentAsString()); } }
代码解释:
- 注入一个 WebApplicationContext 用来模拟 ServletContext 环境
- 声明一个 MockMvc 对象,并在每个测试方法执行前进行 MockMvc 的初始化操作(before)
- 调用 MockMvc 中的 perform 方法开启一个 RequestBuilder 请求,具体的请求则通过 MockMvcRequestBuilder 进行构建,调用 MockMvcRequestBuilder 中的 get 方法表示发起一个 GET 请求,调用 post 方法则发起一个 POST 请求,其他的 DELETE 和 PUT 请求也是一样的,最后通过调用 param 方法设置请求参数
- .andExpect(MockMvcResultMatchers.status().isOk()) 表示添加返回值的验证规则,利用 MockMvcRequestBuilder 进行验证,这里表示验证相应码是否为 200
- .andDo(MockMvcResultHandlers.print()) 表示将请求详细信息打印到控制台
- .andReturn() 表示返回相应的 MvcResult ,并使用 mvcResult.getResponse().getContentAsString() 打印出来
- test2 方法演示了 POST 请求如何传递 JSON 数据,首先使用 om.writeValueAsString(book) 将一个 book 对象转换为一段 JSON,然后设置请求类型 .contentType(MediaType.APPLICATION_JSON) ,最后设置请求内容 .content(s)
除了 MockMvc 这种测试方式之外,Spring Boot 还专门提供了 TestRestTemplate 用来实现集成测试,若开发者使用了 @Spring BootTest 注解,则 TestRestTemplate 将自动可用,直接在测试类中注入即可。注意,如果要使用 TestRestTemplate 进行测试,需要将 @Spring BootTest 注解中 webEnvironment 属性的默认值由 WebEnvironment.MOCK 修改为 WebEnvironment.DEFINED_PORT 或者 WebEnvironment.RANDOM_PORT ,因为这两种都是使用一个真实的 Servlet 环境而不是模拟的 Servlet 环境
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class Test01ApplicationTests { @Autowired TestRestTemplate restTemplate; @Test public void test3() { ResponseEntity8.3.4 JSON 测试hello = restTemplate.getForEntity("/hello?name={0}",String.class,"唐三"); System.out.println(hello); } }
开发者可以使用 @JsonTest 测试 JSON 序列化和反序列化是否工作正常,该注解将自动配置 Jackson ObjectMapper、@JsonComponent 以及 Jackson Modules。如果开发者使用 Gson 代替 Jackson,该注解将配置 Gson。
@RunWith(SpringRunner.class) @JsonTest public class JSONTest { @Autowired JacksonTesterjacksonTester; @Test public void testSerialize() throws IOException { Book book = new Book(); book.setId(1); book.setName("三国演义"); book.setAuthor("罗贯中"); Assertions.assertThat(jacksonTester.write(book)).isEqualToJson("book.json"); 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("三国演义"); } }
代码解释:
- 添加 Jackson 注解(@JsonTest),然后注入 JacksonTester 进行 JSON 的序列化和反序列化测试
- Assertions.assertThat(jacksonTester.write(book)).isEqualToJson(“book.json”) 在序列化完成后判断序列化结果是否是所期待的 json,book.json 是一个定义在当前包下的 JSON 文件。
- Assertions.assertThat(jacksonTester.write(book)).hasJsonPathStringValue(“@.name”) 判断对象序列化之后生成的 JSON 中是否有一个名为 name 的 key
- Assertions.assertThat(jacksonTester.write(book)).extractingJsonPathStringValue(“@.name”).isEqualTo(“三国演义”) 判断序列化后 name 对应的值是否为 “三国演义”
- testDeserialize() 方法是反序列化,反序列化完成是判断对象的 name 属性是否为“三国演义”
- book.json 内容如下
{"id":1,"name":"三国演义","author":"罗贯中"}