背景
先介绍一下我的工作背景,目前在某银行的理财组工作,进组之后就主要负责单元测试的工作,单元测试覆盖率提升至80% 是质量办的要求,也是科技部的工作重点。我们这个部门的要求是85%,我们组的要求是88%,层层加码,苦了我们这些码代码的人,当然也练就了我一身本领!接下来,我将讲述我在工作中遇到的问题,以及对应的解决方案,如果有不妥的地方,请大家口下留情。
问题1 怎么写单测?
啊哈,刚刚进组的时候,确实面临的第一个问题就是这个,当时组内的状况是,组内有两种人,
一种是老程序员,他们往往使用的spring的那种Test,每次跑单测需要启动spring容器,然后使用事务在测试前去插入数据,然后测试的时候呢,就使用这些数据,等到测试后再删除这些数据。
这样测试有以下特点:
- 慢,每次都要启动spring容器,还要何数据库交互,当然慢
- 精确,懂业务的程序员可以通过插入几条数据,就能把代码覆盖提升到80%
- 有环境依赖性,如果数据库的表结构发生了改变,那么这个SQL就可能无法执行,导致单测方法乃至整个单测类报错,这也是之后有时候单测覆盖率会突然降低主要原因
正是基于上一种单测的一些痛点,所以组内要求新来的同事必须使用junit+ mockito 的方式来写单测,这是第二种人,
这样测试有以下特点:
- 不依赖环境,可移植性强,dev环境能跑,uat也能跑
- 运行速度快,不需要启动容器,不需要连接数据库,不需要知道业务的具体细节
- 可能会出现为了单测而单测的情况,不一定能正确反映,因为程序员不懂业务也可以单测
知道了如何写单测接下来就是学习了,自己找个B站的视频,学了mockito的基本原理,知道了各种情况下的插桩,这就基本可以了。具体细节我就不赘述了,列一个清单,大家可以按照关键词去学习
- 什么是插桩
- 有返回值的插桩和无返回值的插桩
- 静态方法的插桩,记得关流,推荐try with resource 的方式关闭
问题2 如何合并代码
使用git时有句话叫做“如果你不知道该怎么办,那么你就来一个分支出来”,这句话反映了git 的优势,固然没错,但是有时候太多分支也会有困扰当时我们的分支是这样的 有每天打版本供测试老师测试的uat 分支,这个分支每天都会有新的代码由dev分支合并,每天都会发版,而我们的单测分支是基于古早的uat分支拉出来的,如果说和入uat 的代码没有动过单测代码的话,那我们往uat合并也是不会有冲突的,但是问题的关键是dev分支写的代码,也会有单测代码,这样就可能造成冲突,导致在合代码的时候有些麻烦。
如何解决?
基于uat分支拉出一个单测分支,每天把uat分支的代码合并到单测分支,如果出现冲突,那么以uat为准,这样可以保证单测分支的代码不会落后uat很多,这样做的好处就是单测代码合并至uat时也基本不会有冲突。
需要注意的是:
如果你在dev分支写业务代码时,若改动了一个类,你需要为这个类(这个类发生了改变,最新的)写单元测试,如果有同事在单测分支也写了这个类(原来的类,老代码)的单测,但是由于dev 的代码是最新的,到时候合并uat的时候就会出现一种情况,一个类有两个单测,一个单测,测的是老的,一个单测测的是新的,这样测时老代码的单测就有可能报错,因为它测试的方法可能就不存在,或者入参已经改变。这在使用sonar扫描的时候就可能导致质量门禁无法通过而无法打出包。
为了避免上述问题(代码合进uat后有语法错误),可以这样:
在uat代码合并至release分支之前呢,先在本地build一下,如果有问题,那么就会在本地build的时候发现,不至于上了jenkins时进行打包的时候再报错,而影响出包的速度。
问题3 一些比较刁钻的类如何插桩?
写单测时看到了太多的逆天代码,太多的永远无法被执行的分支,这个世界是个巨大的草台班子!- 如果一个业务代码需要读文件,该如何插桩?
思路是这样的,如果需要读文件,那么可以造一个文件!但是我们知道本地的文件地址和线上是有差别的关键是如何获取这个文件的路径,具体代码如下,
package com.zhb.album.service.impl;
import org.apache.ibatis.logging.Log;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.io.File;
@RunWith(SpringJUnit4ClassRunner.class)
public class TagServiceImplTest {
@Mock
Log log;
@InjectMocks
TagServiceImpl tagServiceImpl;
@Value("classpath:test.txt")
Resource resource;
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testSelectByInitial() throws Exception {
File file = resource.getFile();
System.out.println(file.getAbsolutePath());
}
}
有几个要点
- 必须使用@RunWith(SpringJUnit4ClassRunner.class)这个注解,其他的行不行
- 使用org.springframework.core.io.Resource 下的resource,
- 弄清楚classpath 的位置 ,classpath 的路径如下

- 如果是静态的工具方法,如何插桩?
要实现这个需要在pom.xml中导入mockito-inline这个包
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
如何实现呢?
package com.zhb.album.service.impl;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mockStatic;
public class StaticMethodStubbingExample {
@Test
public void testStaticMethodStubbing() {
// 使用 mockStatic 创建静态方法的模拟对象
try (MockedStatic<StringUtilsWrapper> mockedStatic = mockStatic(StringUtilsWrapper.class)) {
// 插桩静态方法的行为
mockedStatic.when(() -> StringUtilsWrapper.isEmpty("")).thenReturn(true);
mockedStatic.when(() -> StringUtilsWrapper.isEmpty("hello")).thenReturn(false);
// 调用静态方法并验证结果
boolean isEmpty1 = StringUtilsWrapper.isEmpty("");
boolean isEmpty2 = StringUtilsWrapper.isEmpty("hello");
// 验证静态方法的返回值
assertTrue(isEmpty1);
assertFalse(isEmpty2);
}
}
}
- 如果一个方法调用该类的另一个方法的如何将另一个方法插桩?
public class TagServiceImpl {
public String method1(int a){
String s = this.method2(a);
return String.valueOf(a)+s;
}
public String method2(int a){
return String.valueOf(a);
}
}
@Test
public void test1(){
// 注意使用的是spy 也可以使用@spy注解
TagServiceImpl spyTagService = Mockito.spy(new TagServiceImpl());
when(spyTagService.method2(anyInt())).thenReturn("hello");
String s = spyTagService.method1(111);
System.out.println(s);
}
@Test
public void test2(){
// 注意使用的是spy 也可以使用@spy注解
TagServiceImpl spyTagService = Mockito.spy(new TagServiceImpl());
when(spyTagService.method2(anyInt())).thenAnswer((Answer<String>) invocation -> "666");
String s1 = spyTagService.method1(111);
System.out.println(s1);
}
运行结果如下

- 私有方法如何进行单测?
使用反射
有一个工具类十分方便
org.springframework.test.util下的ReflectionTestUtils
ReflectionTestUtils.invokeMethod(tagServiceImpl, “methodName”, “arg1”,“arg2”,“arg3”);
问题4对于单测工作的小结
- 选对则合适的单测框架很重要
无论是mockito+ mockito Inline 还是 powerMock 还是使用Spring自身的,我觉得都是可以,要跟据这些框架的特点和自身项目的特点去匹配,同时也要考虑各位同事的学习成本。
- 不要只使用一种框架
比如在使用mockito的时候也可以用上junit Springtest等,要结合不同框架的优点来使用。
- 合理使用git
善于使用分支的合并,可以避免很多单测分支和uat分支的冲突问题,各位老师有空可以了解一下rebase和merge , 了解一下revert、undo、chery-pick、stash。

712

被折叠的 条评论
为什么被折叠?



