zoukankan      html  css  js  c++  java
  • 单元测试实践(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.zhihu.com/question/20842791
    asp.net cookie and session
    leelazero and google colab
    download file by python in google colab
    physical processor, core, logical processor
    通过powershell操作eventlog
    openxml in sql server
    get the page name from url
    How to Execute Page_Load() in Page's Base Class?
    Difference between HttpContext.Request and Request
  • 原文地址:https://www.cnblogs.com/pluto4596/p/11703382.html
Copyright © 2011-2022 走看看