单元测试实践(SpringCloud+Junit5+Mockito+DataMocker)

网上看过一句话,单元测试就像早睡早起,每个人都说好,但是很少有人做到。从这么多年的项目经历亲身证明,是真的。
这次借着项目内实施单元测试的机会,记录实施的过程和一些总结经验。

项目情况

首先是背景,项目是一个较大型的项目,多个团队协作开发,采用的是SpringCloud作为基础微服务的架构,中间件涉及Redis,MySQL,MQ等等。新的起点开始起步,团队中讨论期望能够利用单元测试来提高代码质量。单元测试的优点很多,但是我觉得最终最终的目标就是质量,单元测试代码如果最终没有能够提高项目质量,说明过程是有问题或者团队没有真正接纳方法,不如放弃来节省大家的开发时间。
一说到单元测试大家肯定会先想起TDD。TDD(Test Dirven Development,测试驱动开发)是以单元测试来驱动开发的方法论。

  1. 开发一个新功能前,首先编写单元测试用例
  2. 运行单元测试,全部失败(红色)
  3. 编写业务代码,并且使对应的单元测试能够通过(绿色)
  4. 时刻维护你的单元测试,使其始终可运行

一个团队一开始就直接实施TDD的可能性是比较小的,因为适合团队的研发流程、测试底层框架封装、单元测试原则与规范都还没有敲定或者摸索出最佳的实践。直接一开始就完整实施,往往过程会变形,最终目标慢慢会偏离正轨,整个团队也不愿意再接受单元测试。所以建议是逐步开始,让团队切身能够体会到单元测试带来的收益再慢慢加码。

我们的项目基础技术架构是基于SpringCloud,做了一些基础的底层封装。项目之间的调用都是基于Feign,各个项目都是规范要提供各自的Feign接口以及Hystrix的FallbackFactory。我们将对于外部的调用都是封装在底层的service中。

单元测试范围

一个项目需要实施单元测试,首先要界定(或者说澄清)单元测试负责的范围。最常见的疑惑就是与外部系统或者其他中间件的关联,单元测试是否要实际的调用其他中间件/外部系统。
我们先来看看单元测试的定义:

Unit tests are typically automated tests written and run by software developers to ensure that a section of an application (known as the "unit") meets its design and behaves as intended.

单元测试首先应当是自动化的,由开发者编写,为了保证代码片段(最小单元)是按照预期设计实现的。我们理解就是说单元测试要保障的是项目(代码片段逻辑)自身按照设计意图正确执行,所以确认了单元测试的范围仅限于单个项目内部,因此要尽量屏蔽所有的外部系统或中间件。代码的业务逻辑覆盖80%-90%,其他部分(工具类等)不做要求。
我们项目涉及到了一些中间件(Mysql,Redis,MQ等),但是更多涉及到的内部其他支撑系统。用项目内的实际情况我们当前定义的单元测试覆盖的范围就是,单元测试从controller作为入口,尽量覆盖到controller和service所有的方法与逻辑,所有的外部接口调用全部mock,中间件尽量使用内存中间件进行mock。

单元测试基础框架

既然项目是基于SpringCloud,那测试肯定会引入基础的spring-boot-test,底层的测试框架选择是junit。
Junit主流还是junit4(Github地址)最新版本是4.12(2014年12月5日),现在最新的是junit5(JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage)。junit5正式版本的发布日期是2017年9月11日,目前最新的版本是5.5.2(2019年9月9日)。我们项目底层选择了junit5。
目前,在 Java 阵营中主要的 Mock 测试工具有 Mockito,JMock,EasyMock 等。我们选择了Mockito,这个是没有经过特别的选型。简单比较之后选择了比较容易上手并且能够满足当前需求的一款。
redis使用了redis-mock (ai.grakn:redis-mock:0.1.6)
数据库自然是使用h2(com.h2database:h2:1.4.192)(不过在一期项目我们主要服务编排,没有涉及到数据库的实例)
模拟数据生成参考了jmockdata(com.github.jsonzou:jmockdata:4.1.2),但是做了一些小小的调整增加了一些其他的类型

另外,Mockito不支持static的的方法的mock,要使用PowerMock来模拟。但是PowerMock似乎现在还不支持junit5,我们没有使用。

单元测试实施

基本框架搭建完毕,基本就进入了编码阶段。第一期的编码,我们实际上还是先写了业务代码,然后再写单元测试。接下来就详细介绍一下单元测试类的结构。这里给的示例仅仅是我们在实践过程中有使用到的,并非junit5的完整注解或者使用讲解,具体需要了解大家可以参考官网

单元测试基本结构

先看一下头部的几个注解,这些都是Junit5的

// 替换了Junit4中的RunWith和Rule
@ExtendWith(SpringExtension.class)
//提供spring依赖注入
@SpringBootTest
// 运行单元测试时显示的名称
@DisplayName("Test MerchantController")
// 单元测试时基于的配置文件
@TestPropertySource(locations = "classpath:ut-bootstrap.yml")
class MerchantControllerTest{
    private static RedisServer server = null;

    // 下面三个mock对象是由spring提供的
    @Resource
    MockHttpServletRequest request;

    @Resource
    MockHttpSession session;

    @Resource
    MockHttpServletResponse response;

    // junit4中 @BeforeClass
    @BeforeAll
    static void initAll() throws IOException {
        server = RedisServer.newRedisServer(9379);
        server.start();
    }

    // junit4中@Before
    @BeforeEach
    void init() {
        request.addHeader("token", "test_token");
    }

    // junit4中@After
    @AfterEach
    void tearDown() {
    }

    // junit4中@AfterClass
    @AfterAll
    static void tearDownAll() {
        server.stop();
        server = null;
    }

}

这些都是比较基础的注解,基本也和junit4一一对应。这里没有太多可说的,可以看到我们在初始化方法中加载了虚拟的redis服务器,在前置方法中设置了Header的值

单元测试的主体方法

我们测试的主要的就是MerchantController这个类,这个类下面还有一层service方法。先看一下大概的代码印象。

    @Resource
    MerchantController merchantController;

    @MockBean
    private IOrderClient orderClient;

    @Test
    void getStoreInfoById() {
        MockConfig mockConfig = new MockConfig();
        mockConfig.setEnabledCircle(true);
        mockConfig.sizeRange(2, 5);
        MerchantOrderQueryVO merchantOrderQueryVO = Mock.mock(MerchantOrderQueryVO.class);
        StoreInfoDTO storeInfoDTO = Mock.mock(StoreInfoDTO.class,mockConfig);

        Mockito.when(orderClient.bizInfoV3(Mockito.any())).thenReturn(R.data(storeInfoDTO));
        Mockito.when(orderClient.getOrderCount(Mockito.any())).thenReturn(R.data(merchantOrderQueryVO));

        R<StoreInfoBizVO> r = merchantController.getStoreInfoById();

        assertEquals(r.getData().getAvailableOrderCount(), merchantOrderQueryVO.getOrderNum());
        assertEquals(r.getData().getId(), storeInfoDTO.getId());
        assertEquals(r.getData().getBranchName(), storeInfoDTO.getBranchName());
    }

    @ParameterizedTest
    @ValueSource(ints = {1, 0})
    void logoutCheck(Integer onlineValue) {
        MockConfig mockConfig = new MockConfig();
        mockConfig.setEnabledCircle(true);
        mockConfig.sizeRange(2, 5);
        MerchantOrderQueryVO merchantOrderQueryVO = Mock.mock(MerchantOrderQueryVO.class);
        StoreInfoDTO storeInfoDTO = Mock.mock(StoreInfoDTO.class,mockConfig);
        storeInfoDTO.setOnline(onlineValue);
        Mockito.when(orderClient.bizInfoV3(Mockito.any())).thenReturn(R.data(storeInfoDTO));
        Mockito.when(orderClient.getOrderCount(Mockito.any())).thenReturn(R.data(merchantOrderQueryVO));

        R r = merchantController.logoutCheck();

        if (1==onlineValue) {
            assertEquals(ResourceAccessor.getResourceMessage(
                    MerchantbizConstant.USER_LOGOUT_CHECK_ONLINE), r.getMsg());
        } else {
            assertEquals(ResourceAccessor.getResourceMessage(
                    MerchantbizConstant.USER_LOGOUT_CHECK_UNCOMPLETED), r.getMsg());
        }
    }

    @ParameterizedTest
    @CsvSource({"1,Selma,true", "2,Lisa,true", "3,Tim,false"})
    void forTest(int id,String name,boolean t) {
        System.out.println("id="+id+" name="+name+" tORf="+t);
        merchantController.forTest(null);
    }

首先看变量的部分,这里给了两个例子,一个注解是@Resource,这个是让spring来注入的。另外一个是@MockBean,这就是Mockito提供的,并且结合下面的Mockito.when方法。
接下来看方法体,我将方法主体分为三部分:

  1. Mock数据与方法
    使用Mock拦截底层的外部接口方法,并且返回随机的Mock数据(大部分数据可以使用DataMocker生成,有一些特殊有限制的,可以手动生成)。
  2. 测试方法执行
    执行目标测试方法(基本都是一行,直接调用目标方法并且返回结果)
  3. 结果断言
    根据业务逻辑预期进行断言的编写(这部分基本上没有自动化的方式,因为断言的条件和业务逻辑相关只能手动编写)

这样写下来是基本逻辑的验证,还有内部有分支逻辑,如何验证?
代码当中实际上也提到了,就是junit5提供的@ParameterizedTest注解,配合@ValueSource, @CsvSource来使用,分别可以设置指定类型或者复杂类型到单元测试中,使用方法的参数接受,定义测试不同的分支。

单元测试的执行

单元测试的执行实际上分成2部分:

  1. IDE中我们要去验证单元测试是否能够成功执行
  2. CI/CD作为执行的先决条件保障

IDE可以直接指定测试框架,我们选择junit5直接生成单元测试代码,可以直接在测试包或者类上右键执行单元测试。这个方法可以作为我们开发过程中验证待遇测试有效性的手段。但是真正要能在生产开发流程中更好的体现单元测试的价值,还是需要持续集成的支持,我们项目使用的是jenkins。依赖是Maven,以及maven-surefire-plugin插件。要特别注意一点,由于junit5还比较新,所以maven-surefire-plugin插件支持junit5还是稍微有点特殊的,参考官网说明。我们需要引入插件:

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M3</version>
            <configuration>
                <excludes>
                    <exclude>some test to exclude here</exclude>
                </excludes>
            </configuration>
        </plugin>

这样在jenkins构建时就会执行单元测试,如果单元测试失败,不会触发构建后操作(Post Steps)。

总结

目前我们的项目中,单元测试的应用还在第一期,但是投入在上面的时间和精力,实际上到实际开发时间的2-3倍。因为涉及到基础框架的搭建,新框架的引入整合,底层开发编写测试代码的审核,团队的培训等等。我预计在后期,成熟的框架和流程支持下,覆盖核心业务代码的单元测试耗时应该能到实际开发工时的50%-80%左右。但是这部分的投入是能够减少测试以及线上的问题发生的概率,节省了修复的时间。
团队目前还不能完全习惯单元测试的节奏,目前带来的直接益处还不够明显,但是一个好的习惯的养成,还是需要管理者投入精力同时从上而下的推动的。
后期应该对于单元测试的执行还有一些调整或改进,而且对其概念、流程等方面应该也会有更深入和实际的理解。届时还会再次整理,并且分享给大家。

原文地址:https://www.cnblogs.com/pluto4596/p/11703382.html

时间: 2024-11-05 22:34:38

单元测试实践(SpringCloud+Junit5+Mockito+DataMocker)的相关文章

atitit.jndi的架构与原理以及资源配置and单元测试实践

atitit.jndi的架构与原理以及资源配置and单元测试实践 1. jndi架构 1 2. jndi实现原理 3 3. jndi资源配置 3 3.1. resin  <database>  节点 3 3.2. tomcat    <resource 标签 4 3.3. 自定义资源 5 4. JNDI测试支持: 5 4.1. D:\workspace\wxb\src\jndi4t.xml 6 4.2. applicationContext.xml 7 4.3. jdbc.propert

Android单元测试实践

为什么要写单元测试 首先要介绍为什么蘑菇街支付金融这边会采用单元测试的实践.说起来比较巧,刚开始的时候,只是我一个人会写单元测试.后来老板们知道了,觉得这是件 很有价值的事情,于是就叫我负责我们组的单元测试这件事情.就这样慢慢的,单元测试这件事情就成了我们这边的正常实践了.再后来,在公司层面也开始有一定 的推广. 要说为什么要写单元测试的话,我相信大部分人都能承认.也能理解单元测试在保证代码质量,防止bug或尽早发现bug这方面的作用,这可能 是大家觉得单元测试最大的作用.然而我觉得,除了这方面

MVC与单元测试实践之健身网站(一)-项目概述

前不久刚刚通过租房网站的开发学习了MVC,并随后学习了单元测试相关的基础,现在开始健身网站的开发,该项目将结合MVC与单元测试,在开发实践过程中,趁热打铁,巩固并运用之前的内容. 一 健身网站功能描述 关于健身网站的需求,主要从个人日常锻炼的需要出发,以达到辅助锻炼的目的.各应用商店中健身相关的APP也有不少,但始终无法找到一款很好满足增肌训练的.不花哨的.去社交化的.无跑步宗教的应用.于是刚好通过健身网站的开发,学习MVC和单元测试:如果顺利完成的话,也可作为日常锻炼之用. 网站的主要功能是:

MVC与单元测试实践之健身网站(四)-动作管理

网站后台负责进行动作的管理,包括动作名称.介绍.训练要点.配图等内容,以便前台能够使用这些内容.在上一篇< Fit项目图片上传和云存储的调通>中已经准备好了这里涉及到的主要技术难点,现在就开始完成该模块了. 一 列表介绍 健身管理模块包括肌群.肌肉的显示以及动作的管理.这儿也算是开始涉及"业务内容"了,还好我之前有储备了一些关于健身的资料,现在是时候派上另一种用场了. a) 肌群和肌肉因为内容相对固定,所以为了减少业务逻辑以及单元测试的代码量,当然最主要是为了偷懒,就只提供

MVC与单元测试实践之健身网站(二)-管理员模块

开始动手做这个项目时,发现无法做到完全的先设计.再编码,于是决定分模块进行,从管理员模块开始设计.编码,而且接口就已经改了好几次了. 管理员模块涉及的功能有登录和后台对管理员的维护,其中也涉及前端的开发.UI模板使用Inspinia,感觉这套模板功能丰富.界面美观,而且基于HTML5和BootStrap,对这两方面的知识也可以多些了解. 在上一篇<如何在单元测试时隔离ORM>中,解决了对Service层进行测试怎样构建伪对象的问题,随后管理员模块的Service层和单元测试在齐头并进中完成了:

MVC与单元测试实践之健身网站(完)-备案与部署

主页-http://www.zhixin9001.cn/Home/Introduce GitHub- https://github.com/zhixin9001/Fitness 这是关于Fit网站的最后一篇,这几天网站备案审核通过,以后就可以正式使用了. 项目过程中学习了MVC.单元测试.Bootstrap.IOC等内容,也体验了云存储.虚机的使用(共享虚机). 域名备案真是麻烦极了,要在网上提交申请,拍照上传照片,下载打印三张纸质申请单,开始时没看清楚陕西地区的日期不能写还作废了好几张,然后还

MVC与单元测试实践之健身网站(六)-计划的添加与重置

健身计划需要使用者自己定制,没有现成的内容可供选择.本篇就是关于健身计划的添加与重置功能的一部分. 一 功能描述 a) 关于计划的定制,决定以周期的方式,比如有人会以一周为周期,然后安排每周的1.3.5进行锻炼.系统将以以这种模式为典型来实现,首先需要设定一个周期循环的天数,然后为其中的某些天安排特定的锻炼内容,剩下没有安排的天数为休息时间. 为某一天安排具体的锻炼内容时,数据来自通过后台添加的训练项目,这块的功能之前已经完成.因为训练项目分成了两类,一类是对局部肌肉的锻炼,另一类是全身性的综合

MVC与单元测试实践之健身网站(八)-统计分析

?统计分析模块与之前的内容相对独立,用于记录并跟踪各部位围度的变化.还需提供对所作计划的分析,辅助使计划更合理. 一 围度记录 这儿可以记录各项身体围度指标,现在包括体重在内身体上上下下基本全部提供了,虽然貌似用处不大,所以这个界面并不做非空校验,但会有对输入格式的限制,必须是整数或一位小数. 二 围度变化 有了上一步记录的数据后,就可以基于这些数据,以图表的形式直观地反映围度的变化了. a) 模板提供了各种图表插件,最后选择了flotchart,需求比较基础,完全可以满足.用起来也挺方便: 放

MVC与单元测试实践之健身网站(七)-添加计划

?计划的制定涉及到周期-动作包-动作的关联操作,在上一篇<计划的添加与重置>完成了周期的设置.动作包的添加,现在要完成的是动作的添加操作. 一 具体功能 a) 在选定了一个大周期具有的天数后,可以对具体的某一天配置动作包.一个动作包下可以有多个动作,比如3天中的第1天配置了两项动作 b) 点击添加,可以继续增加更多的内容 添加界面糅合了局部训练和综合训练的两套逻辑,不同的训练类型操作界面不同.为了复用一部分html元素和js函数,页面搞得很大,最后js函数仍有20多个,有winform的笨重感