springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo 本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;
文章目录
- @TestConfiguration
- @TestConfiguration 应用实例
- 使用mock方式对controller进行单元测试(无需运行web服务)
- 使用mock方式对controller进行单元测试(无需运行web服务)
- 使用mock方式对controller进行单元测试(需运行web服务且 ==使用webflux==)
- @MockBean 对bean进行mock测试
- 测试json @JsonTest
- @TestPropertySource 对属性配置进行mock
- 参考资料
日志单元测试和日志比较简单,放到一起讲一下。本篇文章需要使用到Junit、TestNg、Mockito、Spring Testing,本文不会对其使用进行特别详细的说明,请自行检索
springboot官方文档中指出,如果我们使用Starters
,那么默认使用Logback
作为日志输出组件。当然还支持Commons Logging, Log4J等组件。
简单日志配置(包含了指定文件目录, 格式,以及level):
logging:
level:
root: info
com.example.controller: info
com.example.service: warn
file: d://a.log
pattern:
console: "%d - %msg%n"
# ----------------------------------------
# CORE PROPERTIES
# ----------------------------------------
debug=false # Enable debug logs.
trace=false # Enable trace logs.
LOGGING
logging.config= # Location of the logging configuration file. For instance, classpath:logback.xml for Logback.
logging.exception-conversion-word=%wEx # Conversion word used when logging exceptions.
logging.file= # Log file name (for instance, myapp.log). Names can be an exact location or relative to the current directory.
logging.file.max-history=0 # Maximum of archive log files to keep. Only supported with the default logback setup.
logging.file.max-size=10MB # Maximum log file size. Only supported with the default logback setup.
logging.group.= # Log groups to quickly change multiple loggers at the same time. For instance, logging.level.db=org.hibernate,org.springframework.jdbc.
logging.level.= # Log levels severity mapping. For instance, logging.level.org.springframework=DEBUG.
logging.path= # Location of the log file. For instance, /var/log.
logging.pattern.console= # Appender pattern for output to the console. Supported only with the default Logback setup.
logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss.SSS # Appender pattern for log date format. Supported only with the default Logback setup.
logging.pattern.file= # Appender pattern for output to a file. Supported only with the default Logback setup.
logging.pattern.level=%5p # Appender pattern for log level. Supported only with the default Logback setup.
logging.register-shutdown-hook=false # Register a shutdown hook for the logging system when it is initialized.
通常只需要在applicatiom.yml中配置即可,但是如果想要对日志进行更加复杂纤细的配置,可能就需要使用到对应日志系统的配置文件了。如果使用logbak,我们只需要在resource中添加logback.xml文件即可(当然下面只是简单实例,详细的logbak的xml配置请读者自行配置):
<configuration debug="false">
<appender name="CONSOLE"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>UTF-8</charset>
<pattern>
%d %-4relative [%thread] %-5level %logger{36} [T:%X{trans}] - %msg%n
</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/demo.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/demo.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
<maxHistory>10</maxHistory>
<maxFileSize>200MB</maxFileSize>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %level %logger{35} [T:%X{trans}] %msg%n</pattern>
</encoder>
</appender>
<span ><span ><span ><span ><</span></span><span ><span >root</span></span></span><span > </span><span ><span ><span >level</span></span></span><span ><span ><span >=</span></span><span ><span ><span >"</span></span></span><span ><span >INFO</span></span><span ><span ><span >"</span></span></span></span><span ><span >></span></span></span>
<span ><span ><span ><span ><</span></span><span ><span >appender-ref</span></span></span><span > </span><span ><span ><span >ref</span></span></span><span ><span ><span >=</span></span><span ><span ><span >"</span></span></span><span ><span >FILE</span></span><span ><span ><span >"</span></span></span></span><span ><span >/></span></span></span>
<span ><span ><span ><span ><</span></span><span ><span >appender-ref</span></span></span><span > </span><span ><span ><span >ref</span></span></span><span ><span ><span >=</span></span><span ><span ><span >"</span></span></span><span ><span >CONSOLE</span></span><span ><span ><span >"</span></span></span></span><span ><span >/></span></span></span>
<span ><span ><span ><span ></</span></span><span ><span >root</span></span></span><span ><span >></span></span></span>
</configuration>
为了方便的使用日志,可以借助spring的@slf4j注解,可以自动注入log,代码中可以直接使用,比较方便:
public class HelloController {
private Stu stu;
private Person person;
<span ><span ></span></span><span >(</span><span ><span >"/properties1"</span></span><span >)</span>
<span ><span ><span >public</span></span></span><span > String </span><span ><span ><span >properties1</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
log<span >.</span><span >debug</span><span >(</span><span ><span >"com.example.controller.HelloController.properties1 执行"</span></span><span >)</span><span >;</span>
log<span >.</span><span >info</span><span >(</span><span ><span >"stu={}"</span></span><span >,</span> stu<span >)</span><span >;</span>
log<span >.</span><span >info</span><span >(</span><span ><span >"person={}"</span></span><span >,</span> person<span >)</span><span >;</span>
<span ><span >return</span></span> <span ><span >"Welcome to springboot2 world ~"</span></span><span >;</span>
<span >}</span>
<span ><span >//省略代码</span></span>
一个Spring Boot application 是Spring ApplicationContext, 在单元测试时没有什么特殊的。
当你使用SpringApplication 时,外部属性,日志等其他功能会被默认装配
springboot提供了@SpringBootTest
注解来辅助我们进行测试。
需要注意:如果我们使用的是JUnit 4 ,那么需要添加
@RunWith(SpringRunner.class)
否则所有注解将会被忽略。
如果你使用的是JUnit5 ,那么在 SpringBootTest
上没有必要添加 @ExtendWith
,因为@…Test
已经添加了ExtendWith
If you are using JUnit 4, don’t forget to also add @RunWith(SpringRunner.class) to your test, otherwise the annotations will be
ignored. If you are using JUnit 5, there’s no need to add the equivalent @ExtendWith(SpringExtension) as @SpringBootTest and the
other @…Test annotations are already annotated with it.
引入test的starter依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
在src/test/java目录下创建MyTest.java
:
(SpringRunner.class)
(classes = {Application.class})// 指定启动类
public class MyTests {
private Person person;
private HelloService helloService;
<span ><span >/**
* 使用断言
*/</span></span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >test2</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
log<span >.</span><span >info</span><span >(</span><span ><span >"test hello 2"</span></span><span >)</span><span >;</span>
TestCase<span >.</span><span >assertEquals</span><span >(</span><span ><span >1</span></span><span >,</span> <span ><span >1</span></span><span >)</span><span >;</span>
<span >}</span>
<span ><span >/**
* 测试注入
*/</span></span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >test3</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
log<span >.</span><span >info</span><span >(</span><span ><span >"person={}"</span></span><span >,</span> person<span >)</span><span >;</span>
log<span >.</span><span >info</span><span >(</span><span ><span >"helloService.getVal()={}"</span></span><span >,</span> helloService<span >.</span><span >getVal</span><span >(</span><span >)</span><span >)</span><span >;</span>
<span >}</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >testBefore</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
System<span >.</span>out<span >.</span><span >println</span><span >(</span><span ><span >"before"</span></span><span >)</span><span >;</span>
<span >}</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >testAfter</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
System<span >.</span>out<span >.</span><span >println</span><span >(</span><span ><span >"after"</span></span><span >)</span><span >;</span>
<span >}</span>
}
我们这里单独执行test3,他会向正常启动springboot服务一样,注入相关的bean,输出如下:
@TestConfiguration
@TestConfiguration
是Spring Boot Test提供的一种工具,用它我们可以在一般的@Configuration之外补充测试专门用的Bean或者自定义的配置。
我们看@TestConfiguration
的定义
ElementType.TYPE)(
(RetentionPolicy.RUNTIME)
public @interface TestConfiguration {
//省略
可见真正起作用的是@TestComponent
:
ElementType.TYPE)(
(RetentionPolicy.RUNTIME)
public @interface TestComponent {
//省略
@TestComponent
用于声明专门用于测试的bean , 他不应该被自动扫描到。也就是说如果你使用@ComponentScan
来扫描bean,那么需要将其排除在外:
excludeFilters = {(
(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
...})
由于@SpringBootApplication
已经添加有排除TypeExcludeFilter的功能,固使用@SpringBootApplication
时不会加载@TestComponent
声明的bean:
ElementType.TYPE)(
(RetentionPolicy.RUNTIME)
(excludeFilters = {
(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@TestConfiguration 应用实例
编写一个bean的创建类:
package config;
import com.example.pojo.Foo;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
public class Config {
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > Foo </span><span ><span ><span >foo</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
<span ><span >return</span></span> <span ><span >new</span></span> <span >Foo</span><span >(</span><span ><span >"from config"</span></span><span >)</span><span >;</span>
<span >}</span>
}
Foo.java:
public class Foo {
private String name;
}
编写测试类(IDEA 可能会在foo属性上标红提示错误,不用管,IDE还没有那么智能,识别不了这里的自动注入):
SpringRunner.class)(
(classes = {Application.class})// 指定启动类
(Config.class)
public class TestConfiguration1 {
<span ><span ></span></span>
<span ><span >private</span></span> Foo foo<span >;</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >testPlusCount</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
log<span >.</span><span >info</span><span >(</span><span ><span >"TestConfiguration1"</span></span><span >)</span><span >;</span>
Assert<span >.</span><span >assertEquals</span><span >(</span>foo<span >.</span><span >getName</span><span >(</span><span >)</span><span >,</span> <span ><span >"from config"</span></span><span >)</span><span >;</span>
<span >}</span>
}
执行这里的testPlusCount
方法,测试通过。
当然上面Config中的注解@TestConfiguration
可以换成@Configuration
效果也是一样的,@TestConfiguration
是专门用于测试的。
使用mock方式对controller进行单元测试(无需运行web服务)
默认情况下,使用@SpringBootTest
不会真正启动web服务,当我们测试controller时,spring测试提供了MockMvc供我们方便的测试controller,就像从浏览器发起请求一样。
在HelloController中有这么一个方法:
"/hello")(
public String hello() {
return "Welcome to springboot2 world ~";
}
启动服务在浏览器中访问:
关闭tomcat服务,我们看如何进行单元测试。
import com.example.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
(SpringRunner.class)
(classes = {Application.class})// 指定启动类
public class MockMvcExampleTests {
private MockMvc mvc;
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >exampleTest</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span ><span ><span >throws</span></span></span><span > Exception </span><span >{</span>
<span ><span >this</span></span><span >.</span>mvc<span >.</span><span >perform</span><span >(</span><span >get</span><span >(</span><span ><span >"/hello"</span></span><span >)</span><span >)</span><span >.</span><span >andExpect</span><span >(</span><span >status</span><span >(</span><span >)</span><span >.</span><span >isOk</span><span >(</span><span >)</span><span >)</span>
<span >.</span><span >andExpect</span><span >(</span><span >content</span><span >(</span><span >)</span><span >.</span><span >string</span><span >(</span><span ><span >"Welcome to springboot2 world ~"</span></span><span >)</span><span >)</span>
<span >.</span><span >andDo</span><span >(</span>MockMvcResultHandlers<span >.</span><span >print</span><span >(</span><span >)</span><span >)</span><span >;</span>
<span >}</span>
}
tomcat服务已经关闭,执行单元测试,输出结果:
2018-12-30 19:29:29.971 INFO 15100 --- [ main] MockMvcExampleTests : Starting MockMvcExampleTests on HIH-D-20265 with PID 15100 (started by hzliubenlong in D:\workspace-wy\springboot2demo)
2018-12-30 19:29:29.973 INFO 15100 --- [ main] MockMvcExampleTests : No active profile set, falling back to default profiles: default
2018-12-30 19:29:31.419 INFO 15100 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2018-12-30 19:29:31.620 INFO 15100 --- [ main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2018-12-30 19:29:31.624 INFO 15100 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
2018-12-30 19:29:31.633 INFO 15100 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 9 ms
2018-12-30 19:29:31.651 INFO 15100 --- [ main] MockMvcExampleTests : Started MockMvcExampleTests in 2.201 seconds (JVM running for 2.974)
MockHttpServletRequest:
HTTP Method = GET
Request URI = /hello
Parameters = {}
Headers = {}
Body = null
Session Attrs = {}
Handler:
Type = com.example.controller.HelloController
Method = public java.lang.String com.example.controller.HelloController.hello()
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[30]}
Content type = text/plain;charset=UTF-8
Body = Welcome to springboot2 world ~
Forwarded URL = null
Redirected URL = null
Cookies = []
2018-12-30 19:29:31.916 INFO 15100 --- [ Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
其中中间的内容为打印的请求详细信息,该测试通过。
使用mock方式对controller进行单元测试(无需运行web服务)
如果您需要启动运行web服务,我们建议您使用随机端口。 如果您使用的是@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT),则可以随机进行测试运行。
这里允许自动注入TestRestTemplate
:
import com.example.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
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.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
•
•
测试基于普通springmvc的运行的controller服务
*/
(SpringRunner.class)
//使用随机端口
(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class RandomPortTestRestTemplateExampleTests {
private TestRestTemplate restTemplate;
public void exampleTest() {
String body = this.restTemplate.getForObject("/hello", String.class);
assertThat(body).isEqualTo("Welcome to springboot2 world ~");
}
}
•
首先启动该springboot应用,然后执行这个单元测试。
使用mock方式对controller进行单元测试(需运行web服务且 使用webflux)
具体的webflux相关的内容后续会讲。这里只需要知道这个springboot提供的是基于reactor的响应式编程(异步非阻塞)架构就行了。而我们之前使用的基于Tomcat的servlet3.1之前的springmvc是同步阻塞的。
要想使用webflux,需要更换spring-boot-starter-web
为spring-boot-starter-webflux
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
- 1
- 2
- 3
- 4
编写测试代码
import com.example.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
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.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
(SpringRunner.class)
//指定使用随机端口(官网推荐的,原因待验证)
(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class RandomPortWebTestClientExampleTests {
/**
*WebTestClient 是用于测试web服务器的非阻塞的响应式客户端
*/
private WebTestClient webClient;
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >exampleTest</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
<span ><span >this</span></span><span >.</span>webClient<span >.</span><span >get</span><span >(</span><span >)</span><span >.</span><span >uri</span><span >(</span><span ><span >"/hello"</span></span><span >)</span><span >.</span><span >exchange</span><span >(</span><span >)</span><span >.</span><span >expectStatus</span><span >(</span><span >)</span><span >.</span><span >isOk</span><span >(</span><span >)</span>
<span >.</span><span >expectBody</span><span >(</span>String<span >.</span><span >class</span><span >)</span><span >.</span><span >isEqualTo</span><span >(</span><span ><span >"Welcome to springboot2 world ~"</span></span><span >)</span><span >;</span>
<span >}</span>
}
首先启动该springboot应用,然后执行这个单元测试。
改为webflux的starter以后,观察启动日志,可以发现不再是基于Tomcat,而是基于netty了
Netty started on port(s): 8080
@MockBean 对bean进行mock测试
在实际项目中,有一些bean可能会调用第三方,依赖外部组件或项目。但是我们单元测试不需要真正调用。那么我们可以使用@MockBean
进行mock结果。
假设HelloService中有调用外部服务的方法:
public interface HelloService {
<span >String </span><span ><span ><span >getVal</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span >;</span>
<span ><span >//模拟远程调用,或者其他服务调用</span></span>
<span >String </span><span ><span ><span >getRemoteVal</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span >;</span>
}
public class HelloServiceImpl implements HelloService{
<span ><span ><span >public</span></span></span><span > String </span><span ><span ><span >getVal</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span >{</span>
<span ><span >return</span></span> <span ><span >"haha"</span></span><span >;</span>
<span >}</span>
<span ><span >//模拟远程调用,或者其他服务调用</span></span>
<span ><span ><span >public</span></span></span><span > String </span><span ><span ><span >getRemoteVal</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span >{</span>
log<span >.</span><span >info</span><span >(</span><span ><span >"真正发起外部请求"</span></span><span >)</span><span >;</span>
<span ><span >return</span></span> <span ><span >"remote Val"</span></span><span >;</span>
<span >}</span>
}
编写单元测试代码:
import com.example.Application;
import com.example.service.HelloService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
•
•
测试bean结果的mock
*/
(SpringRunner.class)
(classes = {Application.class})// 指定启动类
public class MockBeanTest {
//这里使用 @SpyBean 是同样效果
private HelloService helloService;
public void exampleTest() {
//这句的意思是当调用helloService的getRemoteVal方法时,返回mock的结果:"远程调用结果"
given(this.helloService.getRemoteVal()).willReturn("远程调用结果");
<span ><span >//进行调用测试</span></span>
String reverse <span >=</span> helloService<span >.</span><span >getRemoteVal</span><span >(</span><span >)</span><span >;</span>
<span >assertThat</span><span >(</span>reverse<span >)</span><span >.</span><span >isEqualTo</span><span >(</span><span ><span >"远程调用结果"</span></span><span >)</span><span >;</span>
}
}
•
执行单元测试,可以发现并没有真正发起请求。
更多测试相关内容请参见官网 Testing
测试json @JsonTest
import static org.assertj.core.api.Assertions.assertThat;
(SpringRunner.class)
//这里不能使用@SpringBootTest否则报错:Configuration error: found multiple declarations of @BootstrapWith for test class [MyJsonTests]
(classes = {Application.class})
public class MyJsonTests extends AbstractTestNGSpringContextTests {
<span ><span ></span></span>
<span ><span >private</span></span> JacksonTester<span ><span ><</span>Stu<span >></span></span> json<span >;</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >testSerialize</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span ><span ><span >throws</span></span></span><span > Exception </span><span >{</span>
Stu details <span >=</span> <span ><span >new</span></span> <span >Stu</span><span >(</span><span ><span >"马云"</span></span><span >,</span> <span ><span >51</span></span><span >)</span><span >;</span>
<span ><span >// Assert against a `.json` file in the same package as the test</span></span>
<span >assertThat</span><span >(</span><span ><span >this</span></span><span >.</span>json<span >.</span><span >write</span><span >(</span>details<span >)</span><span >)</span><span >.</span><span >isEqualToJson</span><span >(</span><span ><span >"expected.json"</span></span><span >)</span><span >;</span>
<span ><span >// 或者使用基于JSON path的校验</span></span>
<span >assertThat</span><span >(</span><span ><span >this</span></span><span >.</span>json<span >.</span><span >write</span><span >(</span>details<span >)</span><span >)</span><span >.</span><span >hasJsonPathStringValue</span><span >(</span><span ><span >"@.name"</span></span><span >)</span><span >;</span>
<span >assertThat</span><span >(</span><span ><span >this</span></span><span >.</span>json<span >.</span><span >write</span><span >(</span>details<span >)</span><span >)</span><span >.</span><span >extractingJsonPathStringValue</span><span >(</span><span ><span >"@.name"</span></span><span >)</span><span >.</span><span >isEqualTo</span><span >(</span><span ><span >"马云"</span></span><span >)</span><span >;</span>
<span >}</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >testDeserialize</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span ><span ><span >throws</span></span></span><span > Exception </span><span >{</span>
String content <span >=</span> <span ><span >"{\"name\":\"2\",\"age\":\"11\"}"</span></span><span >;</span>
<span >assertThat</span><span >(</span><span ><span >this</span></span><span >.</span>json<span >.</span><span >parse</span><span >(</span>content<span >)</span><span >)</span><span >.</span><span >isEqualTo</span><span >(</span><span ><span >new</span></span> <span >Stu</span><span >(</span><span ><span >"2"</span></span><span >,</span> <span ><span >11</span></span><span >)</span><span >)</span><span >;</span>
<span >assertThat</span><span >(</span><span ><span >this</span></span><span >.</span>json<span >.</span><span >parseObject</span><span >(</span>content<span >)</span><span >.</span><span >getName</span><span >(</span><span >)</span><span >)</span><span >.</span><span >isEqualTo</span><span >(</span><span ><span >"2"</span></span><span >)</span><span >;</span>
<span >}</span>
}
这里不能使用@SpringBootTest否则报错:Configuration error: found multiple declarations of @BootstrapWith for test class [MyJsonTests]
有时候我们会自定义序列化风格,这里对@JsonComponent进行测试:
public class FooJsonComponent {
<span ><span >public</span></span> <span ><span >static</span></span> <span ><span ><span >class</span></span></span><span > </span><span ><span ><span >Serializer</span></span></span><span > </span><span ><span ><span >extends</span></span></span><span > </span><span ><span ><span >JsonSerializer</span></span></span><span ><span ><span ><</span></span><span ><span >Stu</span></span><span ><span >></span></span></span><span > </span><span >{</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >serialize</span></span></span><span ><span ><span >(</span></span></span><span ><span >Stu value</span></span><span ><span ><span >,</span></span></span><span ><span > JsonGenerator gen</span></span><span ><span ><span >,</span></span></span><span ><span > SerializerProvider serializers</span></span><span ><span ><span >)</span></span></span><span >
</span><span ><span ><span >throws</span></span></span><span > IOException</span><span ><span >,</span></span><span > JsonProcessingException </span><span >{</span>
gen<span >.</span><span >writeString</span><span >(</span><span ><span >"name="</span></span> <span >+</span> value<span >.</span><span >getName</span><span >(</span><span >)</span> <span >+</span> <span ><span >",age="</span></span> <span >+</span> value<span >.</span><span >getAge</span><span >(</span><span >)</span><span >)</span><span >;</span>
<span >}</span>
<span >}</span>
<span ><span >public</span></span> <span ><span >static</span></span> <span ><span ><span >class</span></span></span><span > </span><span ><span ><span >Deserializer</span></span></span><span > </span><span ><span ><span >extends</span></span></span><span > </span><span ><span ><span >JsonDeserializer</span></span></span><span ><span ><span ><</span></span><span ><span >Stu</span></span><span ><span >></span></span></span><span > </span><span >{</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > Stu </span><span ><span ><span >deserialize</span></span></span><span ><span ><span >(</span></span></span><span ><span >JsonParser p</span></span><span ><span ><span >,</span></span></span><span ><span > DeserializationContext ctxt</span></span><span ><span ><span >)</span></span></span><span > </span><span ><span ><span >throws</span></span></span><span > IOException</span><span ><span >,</span></span><span > JsonProcessingException </span><span >{</span>
JsonToken t <span >=</span> p<span >.</span><span >getCurrentToken</span><span >(</span><span >)</span><span >;</span>
<span ><span >if</span></span> <span >(</span>t <span >==</span> JsonToken<span >.</span>VALUE_STRING<span >)</span> <span >{</span>
String trim <span >=</span> p<span >.</span><span >getText</span><span >(</span><span >)</span><span >.</span><span >trim</span><span >(</span><span >)</span><span >;</span>
String<span >[</span><span >]</span> split <span >=</span> trim<span >.</span><span >split</span><span >(</span><span ><span >","</span></span><span >)</span><span >;</span>
Stu stu <span >=</span> <span ><span >new</span></span> <span >Stu</span><span >(</span><span >)</span><span >;</span>
stu<span >.</span><span >setName</span><span >(</span>split<span >[</span><span ><span >0</span></span><span >]</span><span >.</span><span >split</span><span >(</span><span ><span >"="</span></span><span >)</span><span >[</span><span ><span >1</span></span><span >]</span><span >)</span><span >;</span>
stu<span >.</span><span >setAge</span><span >(</span>Integer<span >.</span><span >parseInt</span><span >(</span>split<span >[</span><span ><span >1</span></span><span >]</span><span >.</span><span >split</span><span >(</span><span ><span >"="</span></span><span >)</span><span >[</span><span ><span >1</span></span><span >]</span><span >)</span><span >)</span><span >;</span>
<span ><span >return</span></span> stu<span >;</span>
<span >}</span>
<span ><span >return</span></span> <span >(</span>Stu<span >)</span> ctxt<span >.</span><span >handleUnexpectedToken</span><span >(</span><span >handledType</span><span >(</span><span >)</span><span >,</span> p<span >)</span><span >;</span>
<span >}</span>
<span >}</span>
}
/**
•
•
测试自定义的@JsonComponent
*/
(classes = {JsonComponentJacksonTest.class, FooJsonComponent.class})
public class JsonComponentJacksonTest extends AbstractTestNGSpringContextTests {
private JacksonTester<Stu> json;
public void testSerialize() throws Exception {
Stu details = new Stu("zhangsan", 12);
assertThat(this.json.write(details).getJson()).isEqualTo(""name=zhangsan,age=12"");
}
public void testDeserialize() throws Exception {
String content = ""name=zhangsan,age=13"";
Stu actual = this.json.parseObject(content);
assertThat(actual).isEqualTo(new Stu("zhangsan", 13));
assertThat(actual.getName()).isEqualTo("zhangsan");
assertThat(actual.getAge()).isEqualTo(13);
}
}
•
@TestPropertySource 对属性配置进行mock
使用springboot我们通常会将配置设置在application.yml中,但是在测试的时候,可能会对某些配置的值进行修改,接下来我们使用@TestPropertySource来实现这个功能。
使用spring提供的@PropertySource
springboot提供的@ConfigurationProperties
可以加载application.yml中的配置,如果你的配置放到其他目录或者叫其他名称,可以使用@PropertySource
来进行加载。
我们在resources目录下创建两个配置文件:
property-source.properties文件内容是lastName=wanganshi
。property-source.yml内容是lastName: libai
。@PropertySource
可以支持properties和yml两种格式。
编写类PropertySourceConfig.java
来加载配置文件中的内容
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
//支持properties和yml
//@PropertySource("classpath:property-source.properties")
("classpath:property-source.yml")
public class PropertySourceConfig {
}
编写测试类:
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Map;
import static java.util.stream.Collectors.toList;
(SpringRunner.class)
(classes = PropertySourceConfig.class) //加载属性配置
( // 对属性进行设置
properties = {"lastName=abc", "bar=uvw"}
)
public class PropertySourceTest1 implements EnvironmentAware {
<span ><span >private</span></span> Environment environment<span >;</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >test1</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
Assert<span >.</span><span >assertEquals</span><span >(</span>environment<span >.</span><span >getProperty</span><span >(</span><span ><span >"lastName"</span></span><span >)</span><span >,</span> <span ><span >"abc"</span></span><span >)</span><span >;</span>
<span >}</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >setEnvironment</span></span></span><span ><span ><span >(</span></span></span><span ><span >Environment environment</span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
<span ><span >this</span></span><span >.</span>environment <span >=</span> environment<span >;</span>
Map<span ><span ><</span>String<span >,</span> Object<span >></span></span> systemEnvironment <span >=</span> <span >(</span><span >(</span>ConfigurableEnvironment<span >)</span> environment<span >)</span><span >.</span><span >getSystemEnvironment</span><span >(</span><span >)</span><span >;</span>
System<span >.</span>out<span >.</span><span >println</span><span >(</span><span ><span >"=== System Environment ==="</span></span><span >)</span><span >;</span>
System<span >.</span>out<span >.</span><span >println</span><span >(</span><span >getMapString</span><span >(</span>systemEnvironment<span >)</span><span >)</span><span >;</span>
System<span >.</span>out<span >.</span><span >println</span><span >(</span><span >)</span><span >;</span>
System<span >.</span>out<span >.</span><span >println</span><span >(</span><span ><span >"=== Java System Properties ==="</span></span><span >)</span><span >;</span>
Map<span ><span ><</span>String<span >,</span> Object<span >></span></span> systemProperties <span >=</span> <span >(</span><span >(</span>ConfigurableEnvironment<span >)</span> environment<span >)</span><span >.</span><span >getSystemProperties</span><span >(</span><span >)</span><span >;</span>
System<span >.</span>out<span >.</span><span >println</span><span >(</span><span >getMapString</span><span >(</span>systemProperties<span >)</span><span >)</span><span >;</span>
<span >}</span>
<span ><span ><span >private</span></span></span><span > String </span><span ><span ><span >getMapString</span></span></span><span ><span ><span >(</span></span></span><span ><span >Map</span></span><span ><span ><span ><span ><</span></span></span><span ><span >String</span></span><span ><span ><span >,</span></span></span><span ><span > Object</span></span><span ><span ><span >></span></span></span></span><span ><span > map</span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
<span ><span >return</span></span> String<span >.</span><span >join</span><span >(</span><span ><span >"\n"</span></span><span >,</span>
map<span >.</span><span >keySet</span><span >(</span><span >)</span><span >.</span><span >stream</span><span >(</span><span >)</span><span >.</span><span >map</span><span >(</span>k <span >-</span><span >></span> k <span >+</span> <span ><span >"="</span></span> <span >+</span> map<span >.</span><span >get</span><span >(</span>k<span >)</span><span >)</span><span >.</span><span >collect</span><span >(</span><span >toList</span><span >(</span><span >)</span><span >)</span>
<span >)</span><span >;</span>
<span >}</span>
}
测试通过。大家可以将@TestPropertySource
注解去掉来观察输出结果。
对springboot提供的类型安全的属性配置进行mock
前面已经讲过如何进行类型安全的属性配置。这种情况依然可以使用@TestPropertySource
对属性进行mock:
我们使用spring boot 2.1学习笔记【四】属性配置的Person类进行测试。
直接编写测试类:
import com.example.Application;
import com.example.pojo.Person;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
(SpringRunner.class)
(classes = {Application.class})// 指定启动类
(
properties = {"person.lastName=张飞", "person.age=49"}
)
public class PropertySourceTest {
private Person person;
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >test1</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
log<span >.</span><span >info</span><span >(</span>person<span >.</span><span >getLastName</span><span >(</span><span >)</span><span >)</span><span >;</span>
Assert<span >.</span><span >assertEquals</span><span >(</span>person<span >.</span><span >getLastName</span><span >(</span><span >)</span><span >,</span> <span ><span >"张飞"</span></span><span >)</span><span >;</span>
<span >}</span>
}
测试结果通过,大家可以将@TestPropertySource
注解去电观察运行结果。
为单元测试单独提供测试配置
就像上图中那样,我们在src/test/resources目录下创建一个单元测试专用的属性配置文件。就可以在@TestPropertySource
指定加载这个配置即可。
test-property-source.yml文件内容:
testp: 123456789
person:
lastName: abc
对PropertySourceTest1
进行改造:
SpringRunner.class)(
(classes = PropertySourceConfig.class) //加载属性配置
( // 对属性进行设置
properties = {"bar=uvw"},
locations = "classpath:test-property-source.yml"
)
public class PropertySourceTest1 implements EnvironmentAware {
<span ><span >private</span></span> Environment environment<span >;</span>
<span ><span ></span></span><span >(</span><span ><span >"${testp}"</span></span><span >)</span>
String testp<span >;</span>
<span ><span ></span></span>
<span ><span ><span >public</span></span></span><span > </span><span ><span ><span >void</span></span></span><span > </span><span ><span ><span >test1</span></span></span><span ><span ><span >(</span></span></span><span ><span ><span >)</span></span></span><span > </span><span >{</span>
Assert<span >.</span><span >assertEquals</span><span >(</span>environment<span >.</span><span >getProperty</span><span >(</span><span ><span >"lastName"</span></span><span >)</span><span >,</span> <span ><span >"abc"</span></span><span >)</span><span >;</span>
Assert<span >.</span><span >assertEquals</span><span >(</span>testp<span >,</span> <span ><span >"123456789"</span></span><span >)</span><span >;</span>
<span >}</span>
<span ><span >//省略部分代码</span></span>
}
对AOP进行测试
我们这里对HelloService使用AspectJ进行AOP代理:
/**
* AOP
*/
public class HelloAspect {
("execution(* com.example.service.HelloService.getVal())")
public void pointcut() {
}
("pointcut()")
public String changeGetVal(ProceedingJoinPoint pjp) {
return "aopResult";//简单起见,这里直接模拟一个返回值了
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
使用springboot进行配置,启用AOP
(proxyTargetClass = true)//启用aop
("com.example.service")
public class AopConfig {
}
我们队MockMvcExampleTests
添加一个测试方法,验证一下结果:
public void exampleTest1() throws Exception {
this.mvc.perform(get("/hello1")).andExpect(status().isOk())
.andExpect(content().string("aopResult"))
.andDo(MockMvcResultHandlers.print());
}
测试通过,说明代理成功。接下来我们通过另一种方式直接对AOP进行测试,注释已经在代码中写清楚了:
//省略部分import
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.*;
/**
•
•
AOP测试
*/
(SpringRunner.class)
(classes = {Application.class})// 指定启动类
(listeners = MockitoTestExecutionListener.class)//开启Mockito的支持
public class SpringBootAopTest extends AbstractTestNGSpringContextTests {
//声明一个被Mockito.spy过的Bean
private HelloAspect helloAspect;
private HelloService helloService;
public void testFooService() {
//判断helloService对象是不是HelloServiceImpl
assertNotEquals(helloService.getClass(), HelloServiceImpl.class);
<span ><span >//接下来通过AopUtils、AopProxyUtils、AopTestUtils来判断helloService是否是代理的对象</span></span>
<span >assertTrue</span><span >(</span>AopUtils<span >.</span><span >isAopProxy</span><span >(</span>helloService<span >)</span><span >)</span><span >;</span>
<span >assertTrue</span><span >(</span>AopUtils<span >.</span><span >isCglibProxy</span><span >(</span>helloService<span >)</span><span >)</span><span >;</span>
<span >assertEquals</span><span >(</span>AopProxyUtils<span >.</span><span >ultimateTargetClass</span><span >(</span>helloService<span >)</span><span >,</span> HelloServiceImpl<span >.</span><span >class</span><span >)</span><span >;</span>
<span >assertEquals</span><span >(</span>AopTestUtils<span >.</span><span >getTargetObject</span><span >(</span>helloService<span >)</span><span >.</span><span >getClass</span><span >(</span><span >)</span><span >,</span> HelloServiceImpl<span >.</span><span >class</span><span >)</span><span >;</span>
<span >assertEquals</span><span >(</span>AopTestUtils<span >.</span><span >getUltimateTargetObject</span><span >(</span>helloService<span >)</span><span >.</span><span >getClass</span><span >(</span><span >)</span><span >,</span> HelloServiceImpl<span >.</span><span >class</span><span >)</span><span >;</span>
<span ><span >/**
* 但是证明HelloServiceImpl Bean被代理并不意味着HelloAspect生效了(假设此时有多个<span >@Aspect</span>),
* 那么我们还需要验证HelloServiceImpl.getVal的行为。
* 这里调用两次:
*/</span></span>
<span >assertEquals</span><span >(</span>helloService<span >.</span><span >getVal</span><span >(</span><span >)</span><span >,</span> <span ><span >"aopResult"</span></span><span >)</span><span >;</span>
<span >assertEquals</span><span >(</span>helloService<span >.</span><span >getVal</span><span >(</span><span >)</span><span >,</span> <span ><span >"aopResult"</span></span><span >)</span><span >;</span>
<span ><span >//通过MockitoTestExecutionListener来监听是否是调用了两次helloService.getVal()方法</span></span>
<span ><span >//注意这一行代码测试的是helloAspect的行为,而不是helloService的行为</span></span>
<span >verify</span><span >(</span>helloAspect<span >,</span> <span >times</span><span >(</span><span ><span >2</span></span><span >)</span><span >)</span><span >.</span><span >changeGetVal</span><span >(</span><span >any</span><span >(</span><span >)</span><span >)</span><span >;</span>
}
}
•
测试结果是通过。
springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo 本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;