网易首页 > 网易号 > 正文 申请入驻

谈一谈单元测试

0
分享至

作者 | 有尘

写在前面:

对于我们开发人员来说,单元测试一定不会陌生,但在各种原因下会被忽视,尤其是在我接触到的项目中,提测阶段发现各种各样的问题,我觉得有必要聊一下单元测试。

为了写而写的单元测试没什么价值,但一个好的单元测试带来的收益是非常客观的。问题是怎么去写好单元测试?怎么去驱动写好单元测试?

一 我们的现状

现状一:多个项目完全没有单元测试。

现状二:开发人员没有写单元测试的习惯,或者由于赶业务记录而没有时间去写。

现状三:单元测试写成了集成测试,比如容器、数据库,导致单元测试运行时间长,失去了意义。

现状四:太依赖集成测试。

以上是我在aone找的两个项目的测试情况,基本不考虑单元测试就合并发布,形同虚设。

站在开发的角度讲,导致以上问题的原因大概有以下几点:

1.开发成本

对于系统初期,可能要花很多时间去写新业务,对于老系统又太过庞大,无法下手。

2.维护成本

每修改相关的类,或者重构一次代码,我们就要去修改相应的单元测试。

3.ROI

投入产出是不是正收益?可能无论是管理者还是我们开发自己都回质疑这个问题,所以有时候没有强有力的动力。

二 怎么解决

说来说去都是成本的问题,所以我们怎么去解决成本呢?

那么,我们一切从最开始说起:开发的成本

一个单元测试的传统写法,包含以下几个方面:

1.测试数据 (被测数据,和依赖对象)

2.测试方法

3.返回值断言

@Test
public void testAddGroup() {
// 数据
BuyerGroupDTO groupDTO = new BuyerGroupDTO();
groupDTO.setGmtCreate(new Date());
groupDTO.setGmtModified(new Date());
groupDTO.setName("中国");
groupDTO.setCustomerId(customerId);
// 方法
Result result = customerBuyerDomainService.addBuyerGroup(groupDTO);
// 返回值断言
Assert.assertTrue(result.isSuccess());
Assert.assertNotNull(result.getData());
}

一个简单的测试还好,但如果是一逻辑复杂,且入参数据复杂的时候,那写起来其实挺头痛的。怎么解放我们程序员的双手?

“工欲善其事必先利其器”

我们以最大的努力降低我们的开发成本,这就涉及到我们测试框架和工具的选择问题

1 测试框架选择

首先第一个问题就是junit4和junit5的选择,【从junit4到junit5】 我觉得最便利的一个好处就是可以参数化测试,并且基于参数化测试我们可以更加灵活的配置我们的参数。

效果如下:

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}

更好的是,junit5提供了扩展,比如我们常用的json格式。这里我们使用json文件作为输入:

@ParameterizedTest
@JsonFileSource(resources = {"/com/cq/common/KMPAlgorithm/test.json"})
public void test2Test(JSONObject arg) {
Animal animal = JSONObject.parseObject(arg.getString("Animal"),Animal.class);
List stringList = JSONObject.parseArray(arg.getString("List"),String.class);
when(testService.testOther(any(Student.class))).thenReturn(stringArg);
when(testService.testMuti(any(List.class),any(Integer.class))).thenReturn(stringList);
when(testService.getAnimal(any(Integer.class))).thenReturn(animal);
String result = kMPAlgorithm.test2();
//todo verify the result
}

2 mock框架

然后就是其他mock类的框架了

Mockito:语法特别优雅,对于容器类的模拟比较合适,且对于返回值为空的函数调用也提供比较好的断言。缺点是不能模拟静态方法(3.4.x以上版本已支持)

EasyMock:使用方法类似,但是更严格

PowerMock:可以作为Mockito的一个补充,比如要测试静态方法,不过不支持junit5

Spock:基于Groovy语言的单元测试框架

3 数据库层

这里主要介绍一下H2数据库,其基于内存来作为对于关系型数据库的模拟,运行完成自动释放,达到隔离的目的。

主要配置:ddl文件路径、dml文件路径。这里不作详述。

但对于要不要集成数据库,很难去定义,它的作用主要是用来验证sql语法的问题,但是相对来说较重,建议可以用于轻量级的集成测试。

三 Junit5 和 Mockito

后面讲到的自动生成使用的框架和业界使用最多的都是MocKito,所以这里重点介绍一下,包括使用时遇到的问题。

1 使用方法

分别单独引入依赖,推荐引入最新版

1.使用spring-test全家桶

junit5的使用方法这里就不多做介绍,主要说一下这个ArgumentsProvider接口,实现它就可以自定义参数化类,类似于自带的ValueSource、EnumSource等。

2 Mockito 主要注解介绍

先问为什么,为什么需要Mockito

因为:现在的java项目几乎离不开spring框架,而其最为著名的就是IOC,所有的bean用容器来管理,所以这给我们单元测试带来一个问题,如果要对bean做单元测试,就需要启动容器,那么带来的时间的开销将会很大。所以Mockito给我门带来了一系列的解决方法,让我们可以轻松的对bean 进行测试。

假设我们要对上面的A.func()进行单元测试。

@InjectMocks 注解

表示需要注入bean的类,有两种

1.被测试类,这种很容易理解,我们测试这个类,当然也需要向其注入bean。比如上面的A

2.被测试类中的,需要执行其真实的方法,但其里面也要主要bean,也就是上面的C,我们需要测试neeExec方法,但我们不关系B的具体细节。现实中比如事物,并发锁等。这一类需要Mockito.spy(new C())的形式,不然会报错

@Mock

表示要mock的数据,也就是不真实执行其方法内容,只按照我们的规则执行,或者返回,比如使用when().thenReturn()语法。

当然也可以,执行真实方法,则需要when().thenCallRealMethod()方式。

@Spy

表示所有方法都走真实方式,比如有些工具类,转换类,我们也写成了bean的形式(严格来说这种需要写成静态工具类)。

@ExtendWith(MockitoExtension.class)
public class ATest {
@InjectMocks
private A a=new A();
@Mock
private B b;
@Spy
private D d;
@InjectMocks
private C c= Mockito.spy(new C());;

@BeforeEach
public void setUp() throws Exception {
MockitoAnnotations.openMocks(this);
}
@ParameterizedTest
@ValueSource(strings = {"/com/alibaba/cq/springtest/jcode5test/needMockService/A/func.json"})
public void funcTest(String str) {
JSONObject arg= TestUtils.getTestArg(str);
a.func();
//todo verify the result
}

3 Mockito和junit5常见问题

mock 静态方法

mockito3.4以后开始支持,之前的版本可以使用PowerMock辅助使用

Mockito 版本和 java 版本兼容问题

报错如下

Mockito cannot mock this class: xxx
Mockito can only mock non-private & non-final classes.

原因是2.17.0及之前的版本与java8是兼容的

但2.18之后需要使用java11,为了在java8中使用Mockito,则需要引入另一个包

Jupiter-api 版本兼容问题

Process finished with exit code 255
java.lang.NoSuchMethodError: org.junit.jupiter.api.extension.ExtensionContext.getRequiredTestInstances()Lorg/junit/jupiter/api/extension/TestInstance

第一个问题是因为junit5中api、engine、params版本不一致导致的。

第二个问题是因为jupiter-api版本太低的问题,5.7.0以后的版本才支持。

四 测试代码自动生成

选好了框架,我们还是没有解决我们的问题,“怎么节约开发成本?” ,这一节我们来谈这个问题,这也是我主要想表达的。

对于写单元测试,一直以来是比较头痛的事情,要组装各种各样的数据,可能还没跑成功,就被一堆“xxxx不能为null”的报错搞烦了。因此我们有理由去设想,有没有办法去解决这件事情。

1 业界和集团方案调研

在做这个事情之前,肯定是要调研有没有现成的框架。答案是有,但很遗憾,没有找到完全契合我想要的效果,我们来看一下这些插件:

public class BaseTest {
protected TestService testService;
public String baseTest() {
return testService.testBase(1); // 4
}
}
public class JCode5 extends BaseTest {
public void testExtend(){
String s = testService.testOther(new Student()); //1
// 调用 另一个方法
System.out.println(testBean());
// 调用基类方法
baseTest();
}
// 使用testService
public String testBean() {
testService.testMuti(new ArrayList() {{add(1);}}, 2); //2
return testService.getStr(12); //3
}
/**
* 测试范型类
*/
public void testGeneric(Person person) {
//test
list.stream().forEach(a -> {
System.out.println(a);
});
for (int i = 0; i < 2; i++) {
Long aLong = testService.getLong("1213"
, "12323");
System.out.println(aLong);
}
System.out.println(testBean());
}
}
public class TestService {
public String testBase(Integer integer) {
return "TestBase";
}
public List testMuti(List a, Integer c) {
List res = new ArrayList<>();
res.add(a.toString() + c + "test muti");
return res;
}
public String getStr(Integer integer) {
return "TestService" + getInt();
}
public String testOther(Student student) {
return student.getAge() + "age";
}

如上,testExtend一共调用了testService的4个方法,我们对比下各个插件生成的代码。

TestMe

@Test
void testTestExtend() {
when(testService.getStr(anyInt())).thenReturn("getStrResponse");
when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.asList("String"));
when(testService.testOther(any())).thenReturn("testOtherResponse");
jCode5.testExtend(Integer.valueOf(0));
}
@Test
void testTestGeneric() {
when(testService.getStr(anyInt())).thenReturn("getStrResponse");
when(testService.getLong(anyString(), anyString())).thenReturn(Long.valueOf(1));
when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.asList("String"));

jCode5.testGeneric(new Person());
}

1.生成的代码基本符合逻辑,包括需要mock的bean的逻辑都生成了。

2.但它把最重要的一环,也就是数据省略了,只是单纯的用了构造函数的形式。这显然对于我们DDD模型不适应。

3.另外他没用用到junit5的一些特性,比如参数化测试。

4.对于testExtend的方法,它只识别了3个方法。没有识别父类的调用。

JunitGenerate

只能生成基础的框架代码,对于我想mock的逻辑、以及测试方法都没有生成,用处不大。

@Test
public void testTestExtend() throws Exception {
//TODO: Test goes here...
}

Squaretest

生成的方法非常丰富,且一个非常厉害的一点,它能生成多个分支,比如代码逻辑中有if条件,它能生成两个测试,从而走不通的分支。

但是,最大的缺点是“收费软件,不开源”,这就决定了我们没法用它,除非是特别需要。另外测试用过程中还发现了一些其他问题,比如对于继承,重载之类的问题,它解决的也不是很好,往往识别不了需要调用的方法。

虽然无法使用,但还是可以借鉴。

五 打造代码自动生成最佳方案

既然市场上的插件都不是特别合适,就决定写一个适合自己项目的插件(暂时命名JCode5)。有兴趣的也可以自己试试。

1 插件安装

idea插件市场下载,搜索JCode5

2 插件使用

插件有三个功能

1.生成测试代码,也就是生成单元测试。

2.生成json数据,通常用来生成测试数据,比如model。用来参数化测试。

3.增加测试方法,随着业务开发,类可能增加一下功能方法,这个时候相应的可以增加测试方法

定位到需要测试的类,快捷键或菜单定位到generater,如下,选择JCode5.

生成测试类

目前支持三个选项,后续会逐渐完善

另外两个功能类似,直接尝试使用一下就行。

生成的结果---类+json数据

@ParameterizedTest
@ValueSource(strings = {"/com/cq/common/JCode5/testExtend.json"})
public void testExtendTest(String str) {
JSONObject arg= TestUtils.getTestArg(str);
Integer i = arg.getInteger("Integer");
// 识别泛型活着集合类
List stringList = JSONObject.parseArray(arg.getString("List"),String.class);
String stringArg = arg.getString("String");
String stringArg1 = arg.getString("String");
String stringArg0 = arg.getString("String");
// 识别四个方法,包括父类调用、其他方法调用
when(testService.testBase(any(Integer.class))).thenReturn(stringArg);
when(testService.testMuti(any(List.class),any(Integer.class))).thenReturn(stringList);
when(testService.getStr(any(Integer.class))).thenReturn(stringArg0);
when(testService.testOther(any(Student.class))).thenReturn(stringArg1);
jCode5.testExtend(i);
//todo verify the result
}

如上除了生成基本的代码,另外会生成测试数据,它会将该方法所需要的测试数据全都生成在一个json文件当中,完全实现

“数据和代码的分离”

如testExtend.json:

{
"Integer":1,
"String":"test",
"List":[
"test"
]
}

补充判定语句

这一块前期考虑对于不同的方法有不同的校验,所以目前想的还是开发者自己去写验证代码。

注意事项

在自动生成完代码之后,虽然可以运行,但如我们前面提到的,为了写单元测试而写的单元测试是没什么价值的,我们的最终目的是为了写一个好的测试。代码自动生成,但它终究能力有限,所以还是需要我们自己再去验证,比如:

1.该插件生成的代码需要junit5和mockito的支持,使用时需要引入相关的依赖

2.增加assert校验逻辑,看是不是想要的结果,目前插件不会自动生成assertEquals等断言代码。

3.运用参数化测试能力,复制一份生成的json文件并修改输入数据,多组测试


3 插件实现介绍

主要的实现思路,参考了dubbo的SPI的源码,也就是自动实现自适应SPI那部分,简单点说就是反射获取代码逻辑,然后生成测试代码。

4 后期规划

1.mock数据可定制,目前的想法是

  • 固定值比如目前的String: test、Integer和boolean: 0、1

  • 测试者使用配置模版,比如txt文件包含keyValue对

  • 使用Faker,对于name、email、phone这种特定倾向的数据进行特色自动生成

2.自动分支测试,这一块的想法目前主要针对if来做,需要一定的时间。

3.其他

六 写在最后

对于代码自动生成,还是有很多东西可以做的,但有些问题还尚待解决,希望能尽最大努力解放我们的双手,也能提高我们单元测试的质量。

已在我们项目中使用此模式增加135个测试用例(除去mock的单模块达到70%):速度比集成测试(pandora、spring等)提升一个等级。代码的覆盖率相对可观。

从现在开始 学习技术

↓ 2022年 阿里云最新、最热、最全的活动已全面开启

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
嫦娥六号官宣“新发现”!

嫦娥六号官宣“新发现”!

掌上 北京
2024-06-20 08:02:02
91岁老人头顶长“蘑菇”长达五年?竟是一种罕见的恶性肿瘤

91岁老人头顶长“蘑菇”长达五年?竟是一种罕见的恶性肿瘤

澎湃新闻
2024-06-18 13:06:36
对姜萍的质疑上升到老师王闰秋,私下喜好全被扒,知乎被迫清空

对姜萍的质疑上升到老师王闰秋,私下喜好全被扒,知乎被迫清空

王二哥老搞笑
2024-06-19 19:49:13
官宣!中国足协认定马宁判罚错误,漏判大连进球,或遭处罚

官宣!中国足协认定马宁判罚错误,漏判大连进球,或遭处罚

足球慢镜头
2024-06-19 18:11:54
闹大了!人民日报评“数学天才”姜萍冲上热搜,评论神评笑不活了

闹大了!人民日报评“数学天才”姜萍冲上热搜,评论神评笑不活了

起喜电影
2024-06-18 17:04:05
中国海警的立威之战,让菲律宾和美国不敢启动《共同防御条约》?

中国海警的立威之战,让菲律宾和美国不敢启动《共同防御条约》?

战域笔墨
2024-06-19 23:42:55
刘思齐年轻时,妥妥一个小美女,更是一位有情有义的姑娘

刘思齐年轻时,妥妥一个小美女,更是一位有情有义的姑娘

大江
2024-06-13 16:52:21
一个敢问,一个敢答,公司有计划收购贵州茅台吗?回答:没有!

一个敢问,一个敢答,公司有计划收购贵州茅台吗?回答:没有!

财经市界
2024-06-20 08:44:04
花费10亿!入驻在即!快船新球馆最新照:干净又整洁

花费10亿!入驻在即!快船新球馆最新照:干净又整洁

直播吧
2024-06-19 16:40:13
我军公布预算,美国要让中国军费“减半”,军费占GDP多少合适?

我军公布预算,美国要让中国军费“减半”,军费占GDP多少合适?

胡博士聊财经
2024-03-20 09:55:20
董文华当初为何悄然退圈?多年后赖昌星公开说出了两人的关系

董文华当初为何悄然退圈?多年后赖昌星公开说出了两人的关系

星辰故事屋
2024-06-18 18:37:04
人情世故!纳帅换上弗里希&翁达夫,2人在俱乐部主场迎欧洲杯首秀

人情世故!纳帅换上弗里希&翁达夫,2人在俱乐部主场迎欧洲杯首秀

直播吧
2024-06-20 02:58:06
Apple Watch 迎来 10 年首次改款,终于可以换新了!

Apple Watch 迎来 10 年首次改款,终于可以换新了!

刘奔跑
2024-06-17 20:09:35
妻子为男闺蜜当众泼我一杯酒,1天后她道歉,我直言:晚了

妻子为男闺蜜当众泼我一杯酒,1天后她道歉,我直言:晚了

吉祥情感屋
2024-06-12 11:28:14
又被庾澄庆的鞋震撼到了!时隔13年再次断舍离,堆成山的经典款!

又被庾澄庆的鞋震撼到了!时隔13年再次断舍离,堆成山的经典款!

娱小小新
2024-06-20 11:08:55
韩媒:中国游客不文明行为成为过去

韩媒:中国游客不文明行为成为过去

环球时报国际
2024-06-19 08:44:46
上海网友建议:把江苏苏州并入上海市,设苏州为经济特区

上海网友建议:把江苏苏州并入上海市,设苏州为经济特区

记录生活日常阿蜴
2024-06-20 00:50:35
事态升级!党政媒体点评南方医科大学:校规之上应该有人性良知

事态升级!党政媒体点评南方医科大学:校规之上应该有人性良知

芒果的爱pMgf
2024-06-19 23:15:28
精壮了!前辽篮神射贺天举:4个月减了125斤 小小炫耀一下

精壮了!前辽篮神射贺天举:4个月减了125斤 小小炫耀一下

直播吧
2024-06-19 20:56:11
她曾一年换7次男友,风流成性未婚生女,如今41岁无人敢娶

她曾一年换7次男友,风流成性未婚生女,如今41岁无人敢娶

番茄说史聊
2024-06-19 20:46:57
2024-06-20 12:20:49
阿里云云栖号
阿里云云栖号
阿里云官方内容社区!
2943文章数 864关注度
往期回顾 全部

科技要闻

苹果回应AI仅限iPhone15Pro:不是为卖新机

头条要闻

冯德莱恩谋求连任欧委会主席 遭意大利女总理强烈反对

头条要闻

冯德莱恩谋求连任欧委会主席 遭意大利女总理强烈反对

体育要闻

绿军的真老大,开始备战下赛季了

娱乐要闻

离谱!24岁女偶像参加涉毒男星生日聚会,坐在桌边陪赌

财经要闻

茅台大跌,谁的锅?

汽车要闻

售价11.79-14.39万元 新一代哈弗H6正式上市

态度原创

本地
家居
教育
房产
军事航空

本地新闻

中式沙拉宇宙的天花板,它必须有姓名

家居要闻

自然开放 实现灵动可变空间

教育要闻

2023年985院校初次就业率分析,985高校主要就业途径为升学!

房产要闻

海棠湾!一所重量级国际学校真的来了!

军事要闻

以色列涉嫌在加沙使用重型炸弹 或多次违反战争法

无障碍浏览 进入关怀版