Junit5集成测试
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
-
JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
-
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
-
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。
需要的jar (以下使用离线gradle进行管理的,如果是有网络下的maven包的导入简单很多)
- junit-jupiter-5.6.2.jar
- junit-jupiter-api-5.6.2.jar
- junit-jupiter-engine-5.6.2.jar
- junit-jupiter-params-5.6.2.jar
- junit-platform-launcher-1.6.2.jar
- junit-platform-runner-1.6.2.jar
- junit-vintage-engine-5.3.2.jar
- mockito-core-3.9.0.jar
注意事项
jar包版本很容易出现问题,不一定是哪两个jar包之间的不兼容问题,目前我使用这些jar包的组合没有问题
所有的jar包必须放在与模块同级的libs下 ,并在build.gradle中填写相应的依赖信息
基本注解
@Test : 表示方法是测试方法。注意一定要 import org.junit.jupiter.api.Test;
@BeforeEach : 表示在每个单元测试之前执行,多用于初始化
@BeforeEach
void setUp(){
//init somethings....
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@BeforeAll : 表示在所有单元测试之前执行,作用同BeforeEach
@AfterEach : 表示在每个单元测试之后执行
@AfterEach
public void cleanUpEach(){
//destory & clean somethings
System.out.println("After Each cleanUpEach() method called");
}
@AfterAll : 表示在所有单元测试之后执行
@Disabled : 表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
@Timeout : 表示测试方法运行如果超过了指定时间将会返回错误
@ExtendWith : 为测试类或测试方法提供扩展类引用
…
断言
JUnit5 使用了新的断言类:org.junit.jupiter.api.Assertions。
1.断言异常Junit5支持Lambda表达式
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}
2. 超时断言
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
3.参数化测试
参数化测试使使用不同参数多次运行测试成为可能。它们与常规的@Test方法一样被声明,但是使用 @ParameterizedTest注释。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@ParameterizedTest
@ValueSource(strings = { "123", "345", "qwer" })
void palindromes(String str) {
Assertions.assertEquals(String,class,str.getClass());
}
//以上测试结果均为真
4.其余必需参数
@WebAppConfiguration
集成测试所需要的WebApplicationContext容器
@ContextConfiguration
@ContextConfiguration括号里的locations = {“classpath*:application*.xml”},就表示将class路径里的application.xml文件包括进来,里面自动扫描的bean就都可以拿到,此时就可以在测试类中使用**@Autowired**解来获取之前自动扫描包下的所有bean,
没指定xml文件时,默认寻找文件名为 当前类名+“-context.xml"后缀的配置文件
注:@WebAppConfiguration必须与 @ContextConfiguration单个测试类或测试类层次结构一起使用。
@WebAppConfiguration
@ContextConfiguration(locations = {"classpath:application.xml"})
class MyTest{
//...
}
@SpringJunitWebConfig
这个注解类似Junit4中的 @RunWith(SpringJUnit4ClassRunner.class) ,为了让测试在Spring容器环境下执行.
总结:
-
@WebAppConfiguration为测试容器
-
@ContextConfiguration为容器的配置文件
-
@SpringJunitWebConfig指定为集成测试,使用web容器执行测试
综上可知:
集成测试是需要启动web容器,等同于将整个服务器运行起来,所以正常启动项目所需要的资源在测试的容器中同样不可或缺,web容器只会读取自己test模块下的数据库配置信息,等其他配置(一定要保证数据库配置和你预期的数据是一个数据库,不然查处的数据会有差异)
- Controller层测试样例
准备工作
查看模拟接口的完整URL,请求方式,携带参数形式,返回内容,编码格式,请求格式等…
controller层接口
@GetMapping("/plus")
public String plus(@RequestParam(value = "str1") String str1,@RequestParam(value = "str2") String str2){
return serviceTest.plus(str1,str2);
}
@PostMapping("/minus")
public String minus(@RequestBody Map map,@RequestParam String beijing){
System.out.println(map.get("shandong").toString());
return map.get("shandong").toString() + beijing;
}
- get请求:参数只有通过url传递的形式,后端通过 url获取,也可通过
代码样例:
get方式
@SpringJunitWebConfig
@WebAppConfiguration
@ContextConfiguration(locations = {"classpath:application.xml"})
class MyTest(){
// MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
private MockMvc mockMvc;
//这个@Autowired不可以丢失,否则null
@Autowired
private WebApplicationContext context;
//初始化web容器
@BeforeEach
void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
void plus() throws Exception {
//构造一个get request
MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/hello/class")
.param("str1", "1234").param("str2", "qwer").accept(MediaType.APPLICATION_JSON).contentType("application/json;charset=UTF-8");
MvcResult mvcResult = mockMvc.perform(get).andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
//.andExpect(status().isOk()) 表示预计的状态码为200,如果不同则报错
//.andReturn() 返回模拟请求的结果mvcResult,这个参数中包含了request参数,也包括respone参数
//使用断言,判断返回的参数是否与预期相同
Assertions.assertEquals("1234qwer",mvcResult.getResponse().getContentAsString());
}
}
post方式:
该方式大致有两中请求方式:
1.通过URL或.param()传递参数,后端必须用@RequestParam接收参数
2.通过json格式进行参数传递,模拟该参数时应将其放在MockMvcRequestBuilders.post(url).content()中,后端必须用@RequestBody接收
@Test
void show() throws Exception {
MvcResult mvcResult1 = mockMvc.perform(MockMvcRequestBuilders.post("/hellpo/minqus")
.content("{\"shandong\":\"1234\"}").accept(MediaType.APPLICATION_JSON).contentType("application/json;charset=UTF-8")
.param("beijing","5678")
)
.andExpect(status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
System.out.println(mvcResult1.getResponse().getContentAsString());
Assertions.assertEquals("12347778",mvcResult1.getResponse().getContentAsString());
}
- andDo(MockMvcResultHandlers.print())
该方法打印了MockMvc模拟请求的参数格式:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /hello/minus
Parameters = {beijing=[7788]}
Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json", Content-Length:"19"]
Body = {"shandong":"1234"}
Session Attrs = {}
Handler:
Type = com.zc.jutest.controller.HelloController
Method = com.zc.jutest.controller.HelloController#minus(Map, String)
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:"application/json", Content-Length:"8"]
Content type = application/json
Body = 12347788
Forwarded URL = null
Redirected URL = null
Cookies = []
Service层,Dao层,工具类测试样例
Service层,Dao层,工具类样例比较好写了,这里不再写demo了
不过值得注意的是,在做service层的测试或者Dao层测试时,其中涉及到很复杂HttpServletRequest请求,里面的参数很难人为去构造,所以我直接将WebApplicationContext 容器加载进来,使用mvcResult获取request参数,再将request作为Service层,Dao层的方法参数;另外测试工具类时,因为大多数工具类多为静态的,需要使用PowerMock(好像是这个),但是这个并不适配Junit5,望周知.
可能遇到的bug/问题
- 显示读取不到application.xml文件
reload一下gradle就可以了
- 数据库查询失败/依赖导入失败
不要把Test类写在模块下的Test中,因为数据库的配置文件在最外层的Test模块下的,模块内的测试只能读取到本模块内的资源,使用右键用Generate创建的Test类模式是建立在当前模式下的,在setting->Others Setting -> Generate 中可以修改Generate生成Test方法的默认路径(需要安装Generate插件)
单元测试和集成测试区别:
- 单元测试:不需要web容器,只对当前方法进行小范围测试,但是有一些注意事项,比如说需要测试一个controller层接口方法,一般情况下该方法内会调用service层或者其他方法进行数据处理,这又分两种情况:
- 被调用的方法为本类的其他的方法,这里就不需要处理,当前类下的方法是可以调用到的
- 被调用的方法在其他类或者模块,必须使用
Mockito.when(mockObject.method()).thenReturn(object)
模拟该方法行为,将你想要该方法返回的结果值放入object,并且模拟方法行为必须在测试controller层方法前,即提前模拟需要用到的方法.
- 集成测试:需要web容器,和正常启动服务器没区别,测试web容器也需要自己的数据库配置(测试自己的配置),如果同样测试一个controller层接口方法,我们只需要模拟该接口的请求即可,至于方法内调用了哪些方法,怎么执行的,我们不需要关心,只关系最后的返回值;因为集成测试已经启动了整个web应用,所以方法内能正常的执行所有方法调用,返回;
综上所述
单元测试开销更小,速度更快,涉及参数会比较简单,逻辑处理比较简单,适合开发初期测试
集成测试开销会很大,启动整个容器,速度慢,但是可以逻辑复杂,参数冗长的测试,比较适合中后期开发测试
免责声明
以上内容均为本人短期内学习的成果,所写内容为本人个人愚见,都是个人理解,如与事实有出入,轻喷.
每个人都会有一段异常艰难的时光 。 生活的压力 , 工作的失意 , 学业的压力。 爱的惶惶不可终日。 挺过来的 ,人生就会豁然开朗。 挺不过来的 ,时间也会教你 ,怎么与它们握手言和 ,所以不必害怕的。 ——杨绛