以前不太爱写单元测试,觉得浪费时间,最近项目中要求单元测试,因为大多数人修改代码后,以为很简单,都没跑一下就提交了,结果上测试环境后,报一些很简单比如mybatis中的语法编写相关错误,这就是很不细心的问题了,所以还是要做下单元测试;
真正写单元测试后,其实也挺简单,没有现象中的麻烦;
# 单元测试
单元测试简单点就是将你的方法测试一下,看下返回的是否是预期的值;有时候写代码会用main函数跑一下,输出相关结果看看是不是自己要的值;单元测试的时候,就要使用断言来进行判断,断言简单来说,就先预判一下结果是否是自己想要的值,这样程序来帮你看想要的结果是否跟预期的一致;
如果写好一个方法,就通过debug来调试,现在觉得就是对自己写的方法没有清楚认识,一个方法应该是一个比较小的可运行单元,而这个可运行单元就必须是可进行测试;
写单元测试,大家还有一个觉得比较麻烦的就是,改了方法后,单元测试也应该对应着改,这个看似麻烦,但其实也是验证修改前后的必要验证,你应该保证你的代码都是经过验证可行的才可以部署到生产环境;
# junit
在pom.xml
中引入依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
比如我们有以下简单的方法需要测试
public class CommonUtils {
public static String getUUID(){
return UUID.randomUUID().toString();
}
}
新建一个附加Test的同名类,处理当前工具类中的方法测试
public class CommonUtilsTest{
@Test
public void testGetUUID(){
String uuid = CommonUtils.getUUID();
Assert.assertNotNull(uuid);
Assert.assertTrue(uuid.length()==36);
}
}
此单元测试中,就有2个断言,一个是判断uuid
不是null
,第二个判断是对长度进行了判断;
判断方法异常的断言
Assert.assertThrows(IllegalFormatConversionException.class, ()->String.format("hello %c", "cheng"));
// 异常Message
Assert.assertThrows("errorMessage", IllegalFormatConversionException.class, ()->String.format("hello %c", "cheng"));
# spring boot starter单元测试
如果我们写的一个boot starter
的测试,比如需要获取ApplicationContext
后进行操作的一些测试,这时候,加入我们spring-boot
相关的测试依赖即可;
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
boot
项目启动都是有特定注解,此时我们在test
包名下也可以建好一个启动项目(非web
项目的starter
,直接CommandLine
即可)
@SpringBootApplication
public class CoreCommandLineAppTest implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(CoreCommandLineAppTest.class, args);
}
@Override
public void run(String... args) throws Exception {
log.info("coreCommandLineApp is run");
}
}
然后在要测试类上加上注解
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {CoreCommandLineAppTest.class})
public class SpringContextUtilsTest{
@Test
public void getBean() {
ICached cached = SpringContextUtils.getBean(ICached.class);
Assert.assertNotNull(cached);
}
}
此时,我们ApplicationContext
就不会是空了,因为当前测试项目就是一个boot
的启动;
如果有boot
的配置文件,比如application.xml
之类的都可以在test
目录下建相应的配置文件,就跟我们正常配置文件中一些编写即可;
如果是web项目,要测试api,引入的依赖没有变化,改写下我们的启动类即可
@SpringBootApplication
public class CloudCommonApp extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(CloudCommonApp.class,args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return super.configure(builder);
}
}
然后最一个父类,进行相关注解,在要测试的api进行继承父类即可,该相关注解,都是可以在继承子类中进行生效
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {CloudCommonApp.class}, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public abstract class BaseTest {
}
// 具体使用
public class IBaseServerRequestTest extends BaseTest {
private final String host = TestContant.hostPrefix + "/home/";
@Autowired
IBaseServerRequest baseServerRequest;
@Test
public void getServerRequestHelper() {
IServerRequestHelper serverRequestHelper = baseServerRequest.getServerRequestHelper();
final String indexUrl = host + "index";
RequestEntityEx indexRquest = RequestEntityExBuilder.builder()
.setMethod(HttpMethod.GET)
.setUrl(indexUrl).build();
ResponseEntityEx<String> request = serverRequestHelper.request(indexRquest, String.class);
Assert.assertEquals("index", request.getBody());
}
}
# 前置后置测试
在我们正常测试中,先于所有@Test
执行,并在执行完所有@Test
后,在执行后置操作;比如我们在测试某个Api
时,要先建造一些测试数据,然后在逐个Api
测试,最后在删除这些测试数据;
// 具体使用
public class UserApiTest extends BaseTest {
@Before
public void init(){
// 创建测试数据
}
@Test
public void testList(){}
@Test
public void testUpdateUser(){}
@Test
public void deleteUser(){}
@After
public void destroy(){
// 删除测试数据
}
}
# mockito
在以上测试过程中,能编写我们大部分的测试,但是我们对测试还有一个要求,就是代码覆盖率,就是应该让单元测试尽量跑完代码中的所有分支;
比如我们以下代码
public static String getHostName() {
String hostName = "UNHST";
try{
InetAddress addr = InetAddress.getLocalHost();
if(!StringUtils.isEmpty(addr.getHostName())) {
hostName= addr.getHostName();
}
}catch(Exception e){
log.warn("getHostName is error", e);
}
return hostName;
}
此段代码中,有一种情况,就是InetAddress.getLocalHost()
异常了,这个一般我们很难模拟出异常,就需要进行打桩处理,进行测试异常返回;
打桩只是一个说词,意思就是我们对InetAddress对象进行劫持
,并设定如果访问getLocalHost
则抛出一个异常;需要用到第三方包
引入mockito
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
如果是对静态方法打桩,则需要引入mockito-inline
,非静态方法拦截则不需要;
使用如下:
@RunWith(MockitoJUnitRunner.class)
public class CommonUtilsTest{
@Test
public void testGetHostName() {
String hostName = CommonUtils.getHostName();
Assert.assertNotNull(hostName);
try(MockedStatic<InetAddress> mockStatic = mockStatic(InetAddress.class)){
mockStatic.when(InetAddress::getLocalHost).thenThrow(new UnknownHostException());
Assert.assertEquals("UNHST", CommonUtils.getHostName());
}
}
}
以上就在于mockStatic.when(InetAddress::getLocalHost).thenThrow(new UnknownHostException());
这句话,如果访问方法getLocalHost
就抛出一个异常
拦截非静态方法,用途也比较广泛,比如我们有以下两个方法
public class UseServiceImpl{
public String userCalc(){
String result = null;
List<User> users = getList();
// 进行相关逻辑计算
// result = xxx
return result;
}
public List<User> getList(){
// 查询数据库,得到List<User>相关操作
}
}
该用户service的实现中,需要查询数据库,我们现在需要测试的是userCalc
方法,而不想去从库中查询,就需要劫持getList
方法
@RunWith(MockitoJUnitRunner.class)
public class UseServiceImplTest{
@Test
public void testUserCalc() {
List<User> users = new ArrayList<>();
users.add(new User());
try(UseServiceImpl userService = mock(UseServiceImpl.class)){
when(userService.getList()).thenReturn(users);
Assert.assertEquals("xxx", userService.userCalc());
}
}
}
# jmockdata
在测试中,我们需要根据实体类型造出一些模拟数据进行测试,这时候,就需要用到jmockdata框架了,用起来非常简单
引入依赖
<dependency>
<groupId>com.github.jsonzou</groupId>
<artifactId>jmockdata</artifactId>
<version>4.2.0</version>
</dependency>
使用如下
public void test(){
System.out.println(JMockData.mock(int.class));
System.out.println(JMockData.mock(long.class));
System.out.println(JMockData.mock(double.class));
System.out.println(JMockData.mock(float.class));
System.out.println(JMockData.mock(String.class));
System.out.println(JMockData.mock(BigDecimal.class));
}
该相关输出结果都是随机的,无法预测,都是该指定类型的随机;
将对象进行mock
// 将对象进行mock,产生相关数据
User user = JMockData.mock(User.class);
// 进行随机数的一些配置设定
MockConfig mockConfig = new MockConfig().decimalScale(2).sizeRange(5, 10).intRange(1,99).globalConfig();
User user = JMockData.mock(User.class, mockConfig);
# 参考来源
https://yanbin.blog/mockito-3-4-0-mock-static-method/ (opens new window) https://www.cnblogs.com/Ming8006/p/6297333.html (opens new window) https://blog.csdn.net/u011719271/article/details/107778477?ivk_sa=1024320u (opens new window)