Java|谈一谈单元测试

Java|谈一谈单元测试

文章图片

Java|谈一谈单元测试

文章图片

Java|谈一谈单元测试

文章图片

Java|谈一谈单元测试

文章图片

Java|谈一谈单元测试

文章图片

Java|谈一谈单元测试

文章图片



写在前面
对于我们开发人员来说 , 单元测试一定不会陌生 , 但在各种原因下会被忽视 , 尤其是在我接触到的项目中 , 提测阶段发现各种各样的问题 , 我觉得有必要聊一下单元测试 。
为了写而写的单元测试没什么价值 , 但一个好的单元测试带来的收益是非常客观的 。 问题是怎么去写好单元测试?怎么去驱动写好单元测试?
一 我们的现状 现状一:多个项目完全没有单元测试 。
现状二:开发人员没有写单元测试的习惯 , 或者由于赶业务记录而没有时间去写 。
现状三:单元测试写成了集成测试 , 比如容器、数据库 , 导致单元测试运行时间长 , 失去了意义 。
现状四:太依赖集成测试 。


以上是我在aone找的两个项目的测试情况 , 基本不考虑单元测试就合并发布 , 形同虚设 。
站在开发的角度讲 , 导致以上问题的原因大概有以下几点:
1、开发成本
对于系统初期 , 可能要花很多时间去写新业务 , 对于老系统又太过庞大 , 无法下手 。
2、维护成本
每修改相关的类 , 或者重构一次代码 , 我们就要去修改相应的单元测试 。
3、ROI
投入产出是不是正收益?可能无论是管理者还是我们开发自己都回质疑这个问题 , 所以有时候没有强有力的动力 。
二 怎么解决 说来说去都是成本的问题 , 所以我们怎么去解决成本呢?
那么 , 我们一切从最开始说起:开发的成本
一个单元测试的传统写法 , 包含以下几个方面:






























































































【Java|谈一谈单元测试】
测试数据 (被测数据 , 和依赖对象) 测试方法 返回值断言 @Test public void testAddGroup() { // 数据 BuyerGroupDTO groupDTO = new BuyerGroupDTO(); groupDTO.setGmtCreate(new Date()); groupDTO.setGmtModified(new Date()); groupDTO.setName(\"中国\"); groupDTO.setCustomerId(customerId); // 方法 ResultLongresult = 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); ListStringstringList = JSONObject.parseArray(arg.getString(\"ListString\")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 result2 mock框架 然后就是其他mock类的框架了 Mockito: 语法特别优雅 , 对于容器类的模拟比较合适 , 且对于返回值为空的函数调用也提供比较好的断言 。 缺点是不能模拟静态方法(3.4.x以上版本已支持) EasyMock: 使用方法类似 , 但是更严格 PowerMock: 可以作为Mockito的一个补充 , 比如要测试静态方法 , 不过不支持junit5 Spock: 基于Groovy语言的单元测试框架 3 数据库层 这里主要介绍一下H2数据库 , 其基于内存来作为对于关系型数据库的模拟 , 运行完成自动释放 , 达到隔离的目的 。主要配置:ddl文件路径、dml文件路径 。 这里不作详述 。但对于要不要集成数据库 , 很难去定义 , 它的作用主要是用来验证sql语法的问题 , 但是相对来说较重 , 建议可以用于轻量级的集成测试 。三 Junit5和Mockito 后面讲到的自动生成使用的框架和业界使用最多的都是MocKito , 所以这里重点介绍一下 , 包括使用时遇到的问题 。1 使用方法 分别单独引入依赖推荐引入最新版 使用spring-test全家桶 junit5的使用方法这里就不多做介绍 , 主要说一下这个ArgumentsProvider接口 , 实现它就可以自定义参数化类 , 类似于自带的ValueSource、EnumSource等 。2 Mockito 主要注解介绍 先问为什么 , 为什么需要Mockito 因为:现在的java项目几乎离不开spring框架 , 而其最为著名的就是IOC , 所有的bean用容器来管理 , 所以这给我们单元测试带来一个问题 , 如果要对bean做单元测试 , 就需要启动容器 , 那么带来的时间的开销将会很大 。 所以Mockito给我门带来了一系列的解决方法 , 让我们可以轻松的对bean 进行测试 。假设我们要对上面的A.func()进行单元测试 。@InjectMocks注解 表示需要注入bean的类 , 有两种 被测试类 , 这种很容易理解 , 我们测试这个类 , 当然也需要向其注入bean 。 比如上面的A 被测试类中的需要执行其真实的方法 , 但其里面也要主要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 result3 Mockito和junit5常见问题 mock静态方法 mockito3.4以后开始支持 , 之前的版本可以使用PowerMock辅助使用 Mockito版本和java版本兼容问题 报错如下 Mockito cannot mock this class: xxxMockito can only mock non-privatenon-final classes. 原因是2.17.0及之前的版本与java8是兼容的 但2.18之后需要使用java11 , 为了在java8中使用Mockito , 则需要引入另一个包 Jupiter-api版本兼容问题 Process finished with exit code 255java.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 ArrayListInteger() {{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; i2; 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 ListStringtestMuti(ListIntegera Integer c) { ListStringres = 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.StringasList(\"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.StringasList(\"String\")); jCode5.testGeneric(new Person());1、生成的代码基本符合逻辑 , 包括需要mock的bean的逻辑都生成了 。2、但它把最重要的一环 , 也就是数据省略了 , 只是单纯的用了构造函数的形式 。 这显然对于我们DDD模型不适应 。3、另外他没用用到junit5的一些特性 , 比如参数化测试 。4、对于testExtend的方法 , 它只识别了3个方法 。 没有识别父类的调用 。JunitGenerate 只能生成基础的框架代码 , 对于我想mock的逻辑、以及测试方法都没有生成 , 用处不大 。@Testpublic void testTestExtend() throws Exception { //TODO: Test goes here...Squaretest 生成的方法非常丰富 , 且一个非常厉害的一点 , 它能生成多个分支 , 比如代码逻辑中有if条件 , 它能生成两个测试 , 从而走不通的分支 。但是 , 最大的缺点是“收费软件 , 不开源” , 这就决定了我们没法用它 , 除非是特别需要 。 另外测试用过程中还发现了一些其他问题 , 比如对于继承 , 重载之类的问题 , 它解决的也不是很好 , 往往识别不了需要调用的方法 。虽然无法使用 , 但还是可以借鉴 。五 打造代码自动生成最佳方案 既然市场上的插件都不是特别合适 , 就决定写一个适合自己项目的插件(暂时命名JCode5) 。 有兴趣的也可以自己试试 。1 插件安装 idea插件市场下载 , 搜索JCode5 2 插件使用 插件有三个功能 生成测试代码 , 也就是生成单元测试 。生成json数据 , 通常用来生成测试数据 , 比如model 。 用来参数化测试 。增加测试方法 , 随着业务开发 , 类可能增加一下功能方法 , 这个时候相应的可以增加测试方法 定位到需要测试的类 , 快捷键或菜单定位到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\"); // 识别泛型活着集合类 ListStringstringList = JSONObject.parseArray(arg.getString(\"ListString\")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\" \"ListString\":[ \"test\"