发布新日志

  • 随行付微服务测试之单元测试

    2018-12-12 15:06:28


    单元测试为代码质量保驾护航,是提高业务质量的最直接手段,实践证明,非常多的缺陷完全可以通过单元测试来发现,测试金字塔提出者Martin Fowler 强调如果一个高层测试失败了,不仅仅表明功能代码中存在bug,还意味着单元测试的欠缺。因此,无论何时修复失败的端到端测试,都应该同时添加相应的单元测试。 而越早发现发现Bug,造成的浪费就会越小,单元测试本身就能够提供了快速反馈的机制。另外,单元测试是一个优秀的开发工程师必备技能之一,优秀的单元测试是业务快速投产的加速器。

    微服务架构下开展单元测试的意义

    虽然对于100%的单元测试覆盖率我们持有保留态度,但在一个微服务架构基础设施还不完善、开发人员能力参差不齐、DDD(领域驱动设计)能力不足以应对复杂业务的情况下,单元测试是性价比最高的实践。单元测试可以充当一个设计工具,它有助于开发人员去思考代码结构的设计,让代码更加有利于测试,满足架构的可测性设计要求。

    单元测试的意义包括如下内容:

    尽早发现缺陷,降低开发投入成本

    85%的缺陷是代码阶段产生的,单元测试阶段可以发现绝大部分软件缺陷。同时软件产品的缺陷发现的越早往往会大大的降低其开发的投入成本,其缺陷的发现时间与修复缺陷的成本如下图中红色曲线。红色曲线表明随着软件开发的进行,漏洞越早发现,其修复的成本越低,并且其修复成本与开发进度的上升趋势越在后期越接近于指数上升。
    img01.png
    放心重构

    无论是对单体项目还是单体项目向微服务架构迁移,代码都在不断的在变化和重构,通过单元测试,开发可以放心的修改重构代码,减少改代码时心理负担,提高重构的成功率。
    改进设计

    越是良好设计的代码,越容易编写单元测试,多个小的方法的单测一般比大方法(成百上千行代码)的单测代码要简单、要稳定,一个依赖接口的类一般比依赖具体实现的类容易测试,所以在编写单测的过程中,如果发现单测代码非常难写,一般表明被测试的代码包含了太多的依赖或职责,需要反思代码的合理性,进而推进代码设计的优化,形成正向循环。
    选择测试驱动开发(TDD)的模式进行项目开发,以单元测试引导项目实现。这种模式下单元测试先行,根据单元测试代码开发功能代码,进而非常精准的实现业务需求,减少返工和缺陷率,可提高项目质量和效率。

    单元测试的常见误解

    单元测试浪费了太多的时间

    虽然不进行单元测试可以更快的交付到后续测试阶段,但是在后续集成测试阶段、系统测试阶段会发现更多的缺陷甚至软件无法运行的致命缺陷,这些缺陷修复的时间远超过单元测试的时间。另外没有单元测试的代码后期软件进行重构或者改进时花费的时间也比有单元测试的所花费的时间要多很多。所以说完整计划下的单元测试是对时间的更高效的利用。
    已经有接口集成测试、系统功能测试进行质量保证了,集成测试阶段对接口进行全面测试就可以达到单元测试的要求,没必要做重复工作在进行单元测试。

    接口测试和功能测试无法覆盖所有的代码,这样如果缺陷存在则将被遗漏,并且Bug将被带到生产上去。一旦用户使用过程中触发了这些没有测试的代码就会带来严重的经济后果。
    跑通一个业务主流程等价于做过单元测试

    目前有很多开发人员认为,开发完代码之后,写个main方法,从入口调完所有的模块,最后验证下返回结果,就认为做过单元测试了,这种想法是及其错误的,这充其量算一种不全面的冒烟测试,是对单元测试概念的错误认知。
    ##微服务架构下如何开展单元测试
    下面将从单元测试所处的阶段、单元测试用例设计规范、单元测试实现几个维度分别介绍如何在微服务模式下开展单元测试。
    首先看下单元测试所处的阶段,下图为非TDD模式下单元测试所处的阶段

    img02.png
    由图可见单元测试处在特性分支开发完成之后,具体的描述如下:

    1.开发人员从Master分支拉取特性分支作为开发分支;
    2.开发完特性分支后、代码构建、单元测试、静态代码扫描;
    3.通过后合并到Master分支,用于投产。
    下面看下什么样的单元测试用例是优秀的用例,是即满足运行速度又满足高覆盖率的用例。随行付定制了单元测试规范,下面节选了强制要求的部分规范。优秀的单元测试用例要符合以下用例设计规范的要求。

    1.必须遵守 AIR 原则

    【说明】单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
    A:Automatic(自动化)
    I:Independent(独立性)
    R:Repeatable(可重复)
    2.单元测试应该是全自动执行的,并且非交互式的

    【说明】测试框架通常是定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
    3.保持单元测试的独立性

    【说明】为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。反例:method2 需要依赖 method1 的执行,将执行结果做为 method2 的输入
    4.单元测试是可以重复执行的,不能受到外界环境的影响

    【说明】单元测试通常会被放到持续集成中,每次有代码 check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
    5.对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别

    【说明】只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域
    6.核心业务、核心应用、核心模块的增量代码确保单元测试通过

    【说明】新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正
    7.单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下

    【说明】源码构建时会跳过此目录,而单元测试框架默认是扫描此目录
    随行付在推行单元测试落地过程中采用循序渐进的方式,逐步增加单元测试用例达到单元测试规范中规定的覆盖率要求。需要说明的是我们不是追求覆盖率这个数字指标,那样就舍本求末了,我们是通过覆盖率这个可以量化的指标实现提高代码质量的这个根本目的。

    第一阶段:单元测试覆盖率要求至少25%
    第二阶段:单元测试覆盖率要求至少60%
    第三阶段:单元测试覆盖率要求至少80%
    随行付单元测试覆盖率统计同样采用SonarQube平台结合Jenkins工具,Jacoco单元测试覆盖率工具完成,这个同上篇介绍的静态代码扫描流程是一脉相承的。同时要求开发人员本地的IDE工具中安装Jacoco覆盖率插件,当本地开发完单元测试用例并构建后,即可看到覆盖率信息,进而可以快速补充用例,达到覆盖率要求。
    以Eclipse为例,当开发完单元测试代码后,按照如下操作即可查看覆盖率信息。

    1.选择需要统计的java测试代码或者包;
    2.右键,Coverage as->Junit
    3.覆盖率结果会自动在Coverage 视图中展示出来;
    4.在Java编辑器中用不同的颜色标识代码的覆盖情况。
      【说明】 绿色----全覆盖
            红色----未覆盖
            黄色----部分覆盖
    img03.png
    下面介绍下在微服务下应该如何进行单元测试。为了有效的进行单元测试,需要遵循一定的方法,通常采用路径覆盖法设计单元测试用例。所谓路径覆盖法就是选取足够多的测试数据,使程序的每条可能路径都至少执行一次(如果程序图中有环,则要求每个环至少经过一次)。具体设计过程参见如下步骤:

    1.画出程序控制流程图
    2.计算圈复杂度
    3.找出所有程序基本路径
    4.根据路径设计测试数据
    以下图代码为例说明路径覆盖法的设计单元测试的过程

    img04.png
    首先根据代码画出其对应的流程图如下,图中数字代表行号。当条件语句中包含多个条件时应予以拆分,如第13行,拆分为13.1和13.2;对于没有分支和循环的语句可忽略,如第16行。
    img05.png
    有了流程图后,我们可以根据它计算出圈复杂度,这个可以作为测试用例数的上限,圈复杂度计算公式如下:

    V(G)= E - N + 2,E是流图中边的数量,N是流图中结点的数量。 V(G)= P + 1 ,P是流图G中判定结点的数量。

    两个公式用哪个都行,最后的结果应该是一样的。这里我们用第二个公式,V(G)= 3 + 1 = 4,也就是我们只需要设计4条用例即可覆盖所有路径

    接下来就是找出所有基本路径,基本路径是从程序的开始结点到结束可以选择任何的路径遍历,但是每条路径至少应该包含一条已定义路径不曾用到的边,所有的基本路径如下

    A
    B C
    B D E F
    B D E G E F

    得到了所有的基本路径,剩下的简单了,只需要按照路径设计出对应的入参数据即可

    案例1:a = 0, b = 1, 期望值 -1

    案例2:a = 1, b = 0, 期望值 -1

    案例3: a = 4, b = 2, 期望值 2

    案例4:a = 8, b = 12, 期望值 4

    除此之外,单元测试用例设计还需要考虑以下场景:

     边界值
         业务边界
         溢出边界
        字符串、数组、集合等的边界
    异常场景
        业务异常
        输入异常(如参数不合法)
    正常场景
        单个模块的用例设计都可以按照路径覆盖法达到语句覆盖和分支覆盖,但是对于有依赖关系的模块
    在微服务模式下,每个模块之间会存在依赖的情况,为了保持单元测试的独立性原则,在不依赖于外部条件的情况下制造各种输入数据,需要借助Mock技术,其本质是用一个模拟的对象代替真实的对象(例如一个类、模块、函数或者微服务)。模拟对象的行为特征和真实对象非常相似,采用相同的调用逻辑,返回内容按照之前预定义的内容返回,提供返回数据。Mock技术的原理可以用如下案例进行解释。

    img06.png
    当要进行单元测试时,需要给A注入B和C,但是C又依赖D,D又依赖E。这就导致了,A的单元测试不满足独立性原则。 但使用了Mock来进行模拟对象后,就可以把这种依赖解耦,只关心A本身的测试,它所依赖的B和C,全部使用Mock出来的对象,并且给MockB和MockC指定一个明确的行为。

    img07.png
    在单元测试工具的选择方面,随行付单元测试借助Junit工具和Mockito工具进行单元测试,微服务模式下不管是spring boot还是spring cloud,通常使用@SpringBootTest注解进行单元测试。一个单元测试的实现步骤主要包括4步:

    设置测试数据
    Mock依赖的系统并给定预期值,如果没有依赖这步可以省略
    在测试中调用方法
    断言返回的结果是否符合预期
    下面以一个非常简单的例子介绍在微服务模式下如何对spring boot中的controller层和service层进行单元测试。

    img08.png
    调用逻辑简化版如图所示,Controller调用ServiceA,ServiceA依赖ServiceB。

    被依赖ServiceB的代码如下

    package cn.vbill.quality.service;
    import org.springframework.stereotype.Service;

    @Service
    public class ServiceB {
         public boolean serve(int param) {
         return param % 2 == 0;
         }
    }
    被测ServiceA的代码如下

    package cn.vbill.quality.service;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    @Service
    public class ServiceA {
       @Autowired
       private ServiceB srvB;

       public String doSomething(int param) {
          if (srvB.serve(param)) {
            return "even";
          }
        return "obb";
      }
    }
    ServiceA和ServiceB的逻辑非常简单,现在测试ServiceA,步骤如下:

    首先:在gradle中增加测试需要的依赖包

    // 可根据实际情况添加版本号
    testCompile("org.springframework.boot:spring-boot-starter-test")
    其次:在src/test/java下面创建测试类,采用@SpringBootTest注解和Mockito技术对ServiceB进行测试和Mock,更多Mockito的使用可以参考其他文章,这里不过多介绍。代码如下:

     package cn.vbill.quality.service;
     import static org.junit.Assert.assertEquals;
     import static org.mockito.Mockito.when;
     import org.junit.Before;
     import org.junit.Test;
     import org.junit.runner.RunWith;
     import org.mockito.InjectMocks;
     import org.mockito.Mock;
     import org.mockito.Mockito;
     import org.mockito.MockitoAnnotations;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.boot.test.context.SpringBootTest;
     import org.springframework.test.context.junit4.SpringRunner;

     // 以下两个注解在Spring测试中可以说是固定写法
     @RunWith(SpringRunner.class)
     @SpringBootTest
     public class ServiceATest {
     @InjectMocks  //创建被测试类实例
     @Autowired
     private ServiceA srvA; // 先自动装配ServiceA,然后用Mock的ServiceB替换原来的ServiceB

     @Mock
     private ServiceB srvB; // 自动生成ServiceB的Mock实例

     @Before
     public void setup() {
        // 必须在ServiceA完成自动装配后在调用此方法
        // 处理@Mock注解,注入Mock对象
        MockitoAnnotations.initMocks(this);
     }

     // 用Mock对象替换真实的ServiceB可以轻松创造出我们所需的场景
     @Test
     public void doSomething_Even_Success() {
        // 设置Mock预期值
        when(srvB.serve(Mockito.anyInt())).thenReturn(true);
        // 因为Mock的缘故,此处doSomething的实参可随意写
        String result = srvA.doSomething(0);
        // 验证预期值
        assertEquals("even", result);
      }

    @Test
    public void doSomething_Obb_Success() {
        // 覆盖另一条分支
        when(srvB.serve(Mockito.anyInt())).thenReturn(false);
        String result = srvA.doSomething(0);
        assertEquals("obb", result);
      }

    }
    最后,使用覆盖率工具查看单元测试覆盖率,如下图所示,实现了100%覆盖。

    img09.png
    ServiceB没有任何依赖,因此对它测试就按照常规的Junit测试即可,这里不过多介绍。下面介绍Controller层的单元测试,整体上看 Controller 层的测试和 Service 层大致相同,只不过是我们不去直接调用 Controller 的方法,而是通过MockMvc模拟HTTP请求。从逻辑图上看Controller是直接调用ServiceA,因此需要使用Mockito模拟ServiceA。

    被测Controller代码逻辑如下:

    package cn.vbill.quality.web;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;

    import cn.vbill.quality.service.ServiceA;

    @RestController
    @RequestMapping("/")
    public class DemoController {
    @Autowired
    private ServiceA srvA; // ServiceA 代码见上一节

    @GetMapping
    public String doSomething(@RequestParam("p") Integer param) {
        return srvA.doSomething(param);
      }
    }
    测试类如下

    package cn.vbill.quality.web;

    import static org.mockito.Mockito.when;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.InjectMocks;
    import org.mockito.Mock;
    import org.mockito.Mockito;
    import org.mockito.MockitoAnnotations;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc // 使用该注解自动配置 MockMvc
    public class DemoControllerTest {
     @Autowired
     @InjectMocks
     private DemoController controller;

     @Mock
     private ServiceA srvA;

    @Autowired
    private MockMvc mvc; // 自动配置 MockMvc

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void doSomething_Success() throws Exception {
        when(srvA.doSomething(Mockito.anyInt())).thenReturn("mock");
        // 使用MockMvc模拟HTTP请求
        // 下面三个类经常使用,常通过静态导入简化代码
        // MockMvcRequestBuilders, MockMvcResultHandlers, MockMvcResultMatchers
        mvc.perform(get("/").param("p", "1")).andExpect(content().string("mock"));
    }
    }
    最后,通过覆盖率工具查看单元测试覆盖率为100%,做到了全覆盖。

    img10.png
    以上是如何在微服务模式下进行单元测试进行了详细的介绍,在微服务架构下高覆盖率的单元测试是保障代码质量的第一道也是最重要的关口,应该持之以恒。

    总结

    本篇分别从微服务模式下开展单元测试的意义、对单元测试的常见误解以及如何开展单元测试三个方面进行介绍,单元测试是一项成本低、收益高的实践,要利用好这把利剑,打好代码质量基础,为后续的质量保证过程添砖加瓦。
  • 随行付微服务测试之接口测试和契约测试

    2018-12-12 14:48:49

    日常开发过程中,项目的接口通常由服务提供方约定和提供,微服务模式下接口被多个消费者调用更是常态,那么提供方接口的变更如何快速、高效、无遗漏的通知给消费者呢?另外,当一个service同时被多个使用者调用,如何保证对service的修改可以让其它所有使用者造成的影响都能被感知到?这些问题契约测试可以给你答案。另外,微服务模式下,接口测试是非常重要的测试手段,它在实际的项目中帮助验证微服务之间的协同和交互,大幅降低测试成本和提高测试效率方面提供了很大帮助,可以说接口测试是业务功能测试前置的助推器。因此,这里对这两种测试手段进行介绍。


    接口测试和契约测试所处的阶段

    在实际的工作中,结合随行付的实际情况我们对自动化测试金字塔原理进行了定制,加入契约自动化测试内容,形成如下新版自动化测试金字塔结构。




    由图可知,一个项目的测试过程,从项目推进的维度,首先进行单元测试,其次接口自动化测试、契约测试,最后UI自动化测试和手工测试。


    微服务模式下如何开展接口测试

    接口测试属于集成测试范畴,他是单元测试的扩展和延续。它主要的关注点是内部接口功能实现是否完整,比如说内部逻辑是不是正常,异常处理是不是正确。它是单元测试和契约测试的过渡阶段,它是项目单个代码逻辑最终串联形成有价值业务逻辑的桥梁。因此,其作用举足轻重。随行付开展接口测试,采用的思路是规范和方法先行,其次是工具选择、人员培训,然后是实施和过程优化,最后常态化持续提效和质量保证的过程。


    接口测试规范化要求

    接口测试的质量保证和测试过程的流程化需要通过规范和方法进行指导和约束。我们定制了如下要求(部分内容):


    需求存在新增接口或者接口变更时,要求进行新增接口测试案例的编写或存量接口案例的维护;


    需求涉及到的存量接口需要进行回归测试;


    接口测试覆盖率要求达到100%;


    需求测试结束前至少进行一轮接口回归测试,且回归通过率达到100%


    测试流程规范涉及从需求提出、脚本编写、执行到测试报告的各个过程。


    接口文档。接口文档是接口测试案例设计的依据,接口文档的全面性和准确性决定了接口测试范围的全面性和接口测试结果的正确性、有效性。随行付采用swagger进行接口文档管理。


    接口用例设计。根据接口文档设计接口测试案例,接口测试案例通过接口测试平台进行编写,且需要满足不重不漏原则。


    接口用例评审。根据项目实际情况,接口测试案例编写完成后,需组织相关干系人进行案例评审,记录并发送会议纪要。


    接口用例执行。需求测试结束前接口测试案例至少在测试环境中执行了一次回归测试,要求案例执行通过率达到100%


    缺陷管理和测试报告。


    脚本纳入回归体系,定时回归,持续保障接口的质量,以及接口质量的持续和及时反馈。


    脚本命名规范和编写规范如下(部分内容):


    接口命名要求:采用“接口名称_接口描述”进行命名,用于定义唯一接口。


    方法命名要求:采用“方法名_描述”进行命名,用于定义唯一方法。


    案例命名要求:采用“序号场景操作期望结果”进行命名,用于定义唯一案例。


    【强制】每个接口测试案例都必须包含至少一个断言;


    【强制】对于json格式的报文,接口入参和断言响应的预期值需要使用严格的json格式;


    【强制】swagger脚本导入到接口测试平台时,需要导入.json文件,且文件内容为无BOM的UTF-8编码;


    【强制】数据初始化和断言的sql必须带where条件,且能唯一定位到期望的数据;


    【强制】数据库回退的sql必须带where条件,且能唯一定位到需要回退的数据;


    【强制】影响公共表(如:TBAPCDE_BNK表)或者其他组数据库表(如:资金组)的sql,在数据初始化、回退、接口影响的数据回退、断言回退时必须严格审查;


    【强制】数据库断言sql中的where条件的主键组合需要放到前面,用于断言失败时快速定位问题;


    接口测试用例设计要求

    为了保证接口的质量,需要进行全面的接口测试,因此在涉及接口测试用例时需要依赖方法,因此我们总结了接口测试用例的设计要求,如下图所示。




    接口测试工具

    接口测试过程提效、测试过程自动化需要依赖自动化测试工具,武器不好很难打胜仗。经过调研,市面上很多接口自动化测试工具均无法满足所有的测试要求,因此我们自研了接口自动化测试平台。自动化测试平台具有如下能力:


    案例自动生成。http/https接口案例自动化生成和导入。

    测试过程集中可视化管理。通过将自动化测试过程web化实现了自动化测试计划、自动化测试用例编写、自动化测试用例执行、自动化测试用例管理和自动化测试报告管理各个过程的可视化。

    模拟性能场景。自动化测试实现了通过接口案例模拟性能测试场景的能力。通过使用平台中提供的接口案例,进行并行执行模拟性能场景。

    多协议多报文类型支持。支持http/https协议、dubbo协议、socket协议、rabbitMQ协议等协议的自动化测试,并支持对协议的扩展。同时支持xml、json、sop、8583等多种报文类型以及报文类型的扩展。

    测试资产有效积累。

    自动化调度执行和邮件发送。自动化测试执行通过定时对案例进行调度执行,可对指定的构建版本对应的案例进行自动化的分批、定时调度执行并邮件发送测试报告。

    系统质量的可视化反馈。通过对自动化案例的执行结果统计,分析出系统的质量趋势,做到系统质量的持续化反馈。通过根因分析,统计系统问题的根本原因的比例,更有针对性的解决质量问题。


    通过接口测试持续运行1年多的持续运营,随行付核心业务接口基本实现接口测试用例全覆盖,且均纳入到定期回归过程,持续为接口的质量保驾护航。


    微服务模式下如何开展契约测试

    契约测试的价值


    契约测试分两种类型,一种是消费者驱动,一种是提供者驱动。其中最常用的,是消费者驱动的契约测试(Consumer-Driven Contract Test,简称 CDC)。核心思想是从消费者业务实现的角度出发,由消费者端定义需要的数据格式以及交互细节,生成一份契约文件。然后生产者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证该实现结果是否正确。对于基于Restful API的微服务来说,它的契约就是指 API 的请求和响应的规则。 如下图所示:



    对于请求,包括请求 URL 及参数,请求头,请求内容等;

    对于响应,包括状态码,响应头,响应内容等。

    对于元数据,指对消费者与提供者间一次协作过程的描述。譬如消费者/提供者的名称、上下文及场景描述等。


    那么契约测试能给微服务带来什么价值呢?文章开头已经提到了契约测试的一部分价值,即接口变更快速通知,servise修改的快速感知。除此之外,它还带来下列价值:


    降低服务集成的难度。把服务集成这个过程分解成了更细的单元测试和接口测试,它从消费者的需求为出发点,把消费者的需求作为测试用例驱动实现一份契约,然后验证提供者端的功能。

    开发并行,提高开发效率。契约隔离了消费者和提供者,双方可以并行开展工作,开发过程中就利用契约进行预集成测试,不用等到联调再来集成调通接口,一旦**,在保证质量的前提下,联调的成本可以减低到几乎为0。

    确保变动的安全性和准确性。只要有变化,契约测试即可第一时间发现,保证安全和对接的准确性。

    作为Mock server为消费者提供Mock服务。集成测试为服务者提供


    微服务下如何开展契约测试


    随行付采用在Spring Cloud Contract开展契约测试。其核心流程包括2步:


    对消费者的业务逻辑进行验证时,先对其期望的响应做模拟提供者(Mock);并将请求(消费者)-响应(基于模拟提供者)的协作过程,记录为契约;

    通过契约,对提供者进行回放,保证提供者所提供的内容满足消费者的期望。

    下面用一个简单的例子说明设计契约测试的方法。这个例子中,一个微服务提供了一个包含三个字段(“IP”、“name”和“password”)的资源,供三个消费者微服务使用。这三个微服务分别使用这个资源中的不同部分。消费者 A 使用其中的 IP 和 name 这两个字段。因此,测试脚本中将只验证来自提供者的资源中是否正确包含这两个字段,而不需要验证 password 字段。消费者 B 使用 IP 和 password 字段,而不需要验证 name 字段。消费者 C 则需要确认资源中包含了所有这三个字段。现在,如果提供者需要将 name 分为姓(first name)和名(last name),那么就需要去掉原有的 name 字段,加入新的 first name 字段和 last name 字段。这时执行契约测试,就会发现消费者 A 和 C 的测试用例就会失败。测试用例 B 则不受影响。这意味着消费者 A 和 C 服务的代码需要修改,以兼容更新之后的提供者。修改之后,还需要对契约内容进行更新。



    下面以一个例子介绍如何使用Spring Cloud Contract开展契约测试的。


    1. Spring Cloud Contract契约是用基于Groovy的DSL定义的,下面是一个契约测试的代码段

     package contracts

      org.springframework.cloud.contract.spec.Contract.make {

      request {

        method 'PUT'

        url '/fraudcheck'

        body([

               "client.id": $(regex('[0-9]{10}')),

               loanAmount: 99999

        ])

        headers {

            contentType('application/json')

        }

       }

     response {

        status OK()

        body([

               fraudCheckStatus: "FRAUD",

               "rejection.reason": "Amount too high"

        ])

        headers {

            contentType('application/json')

        }

       }

     } 

    2. 服务方-server (HTTP) / producer (Messaging) 端增加Spring Cloud Contract Verifier的gralde插件,它用于解析契约文件生成测试。生成测试的命令。

    ./gradlew generateContractTests  

    下面是自动生成的测试脚本


    @Test

    查看(1073) 评论(0) 收藏 分享 管理

Open Toolbar