之前分享了关于质量内建的话题关于单元测试引起了大家的讨论,对于单元测试这件事情本身是比较熟悉的,但大家的反馈是比较难执行,矛盾在于很多测试做不了单元测试,或者让测试做性价比不是很高,这件事情推给开发之后又容易不了了之,其中一个很重要的点是,测试和开发没有同频对话的能力,各种细节难以敲定,落地的实际价值不容易度量,所以这篇文章我就基于常见的springboot框架,聊一聊单元测试分层的几种实践方式,从测试的视角给同学们一些知识面的拓展,也让大家熟悉下单元测试的常见玩法。
一.单元测试带来的好处
1.预防bug
为什么说可以预防bug呢,如果能够执行单元测试,说明开发已经具备一定的质量思维了,在写代码的时候会考虑如何测试,有哪些测试点等,通过这样的思维可以预防bug的产生。
2.快速定位Bug
单元测试意味着我们测试的前置以及测试颗粒度的细化,所以更容易在更小范围内锁定bug,能够带来效率的提升,相对于在测试阶段发现bug来说,会大量减少调试时间。
3.降低重构风险
快速的发现并解决问题不容易形成技术债,团队具备良好的质量把控意识会从根本上带来质量的提升,从而降低重构的可能性。
二.SpringBoot的测试库
SpringBoot提供了如下的类库,通过引入可以获取到测试的类方法。
代码语言:javascript复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Junit:Java应用程序单元测试标准类库
AssertJ:轻量级断言类库
Mockito: Java的Mock测试框架
JsonPath:JSON操作类库
JSONNAssert:基于JSON的断言库
三.快速创建单元测试
当我们引入spring-boot-starter-test相关的类库后,直接在工程项目中src/test/java中创建类即可,如下所示:
代码语言:javascript复制package com.example.demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UnitTestDemoApplicationTests {
@Test
public void contextLoads() {
}
}
@SpringBootTest:是SpringBoot用于测试的注解,可指定入口类和测试环境。
@RunWith(SpringRunner.class):让测试运行于Spring的测试环境。
@Test 表示为一个测试单元。
四:SpingBoot基础知识
先来简单看下我们如何访问springboot服务,当用户通过浏览器访问后端服务时,通过Controller层决定控制访问逻辑,Service层主要实现系统的业务逻辑,DAO层直接操作数据库的代码。
总结这三者,通过例子来解释:
Controller像是服务员,顾客点什么菜,菜上给几号桌,都是ta的职责;
Service是厨师,action送来的菜单上的菜全是ta做的;
Dao是厨房的小工,和原材料打交道的事情全是ta管。
五.单元测试的分层实践
1.基于Controller层的单元测试
关于实践就直接通过代码演示,首先可以在controller层实现一下demo,在src/test/java下完成
代码语言:javascript复制package com.example.demo.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(String name){
return "hello " name;
}
}
解释下:
@RestController:代表这个类是REST风格的控制器,返回JSON或者XML的类型数据。
@RequestMapping:用于配置URL和方法之间的映射,可用在类和方法上。
编写测试代码:
代码语言:javascript复制package com.example.demo.controller;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.junit.Assert.*;
@SpringBootTest
@RunWith(SpringRunner.class)
public class HelloControllerTest {
//启用web上下文
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setUp() throws Exception{
//使用上下文构建mockMvc
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void hello() throws Exception {
// 得到MvcResult自定义验证
// 执行请求
MvcResult mvcResult= mockMvc.perform(MockMvcRequestBuilders.get("/hello")
//.post("/hello") 发送post请求
.contentType(MediaType.APPLICATION_JSON_UTF8)
//传入参数
.param("name","cctester")
// .accept(MediaType.TEXT_HTML_VALUE))
//接收的类型
.accept(MediaType.APPLICATION_JSON_UTF8))
//等同于Assert.assertEquals(200,status);
//判断接收到的状态是否是200
.andExpect(MockMvcResultMatchers.status().isOk())
//等同于 Assert.assertEquals("hello cctetser",content);
.andExpect(MockMvcResultMatchers.content().string("hello cctester"))
.andDo(MockMvcResultHandlers.print())
//返回MvcResult
.andReturn();
//得到返回代码
int status=mvcResult.getResponse().getStatus();
//得到返回结果
String content=mvcResult.getResponse().getContentAsString();
//断言,判断返回代码是否正确
Assert.assertEquals(200,status);
//断言,判断返回的值是否正确
Assert.assertEquals("hello cctester",content);
}
}
通过相应的输出可以完成校验,可以看到controller级别的单元测试跟接口测试比较接近
2.Service层的单元测试
我们还是先来创建demo,先来一个实体类
代码语言:javascript复制package com.example.demo.entity;
import lombok.Data;
@Data
public class User {
private String name;
private int age;
}
再创建服务类,@Service来标注服务类
代码语言:javascript复制package com.example.demo.service;
import com.example.demo.entity.User;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public User getUserInfo(){
User user = new User();
user.setName("cctester");
user.setAge(28);
return user;
}
}
编写测试用例
代码语言:javascript复制package com.example.demo.service;
import com.example.demo.entity.User;
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.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.hamcrest.CoreMatchers.*;
//表明要在测试环境运行,底层使用的junit测试工具
@RunWith(SpringRunner.class)
// SpringJUnit支持,由此引入Spring-Test框架支持!
//启动整个spring的工程
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void getUserInfo() {
User user = userService.getUserInfo();
//比较实际的值和用户预期的值是否一样
Assert.assertEquals(18,user.getAge());
Assert.assertThat(user.getName(),is("cctester"));
}
}
运行结果:
3.DAO层的单元测试
DAO层主要用于对数据的增删改查操作,同样可以进行单元测试,并使用@Transactional注解进行回滚操作,我们也来简单演示下
代码语言:javascript复制package com.example.demo.repository;
import com.example.demo.entity.Card;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
// SpringJUnit支持,由此引入Spring-Test框架支持!
//启动整个spring的工程
@SpringBootTest
//@DataJpaTest
@Transactional
//@Rollback(false)
public class CardRepositoryTest {
@Autowired
private CardRepository cardRepository;
@Test
public void testQuery() {
// 查询操作
List<Card> list = cardRepository.findAll();
for (Card card : list) {
System.out.println(card);
}
}
@Test
public void testRollBank() {
// 写入操作
Card card=new Card();
card.setNum(3);
cardRepository.save(card);
//throw new RuntimeException();
}
运行testRollBack,可以看到输出台
代码语言:javascript复制hibernate: insert into cardtestjpa (num) values (?)
2022-04-17 09:21:25.866 INFO 27907 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@3bb9a3ff testClass = CardRepositoryTest, testInstance = com.example.demo.repository.CardRepositoryTest@2bf3ec4, testMethod = testRollB