cover_image

玩物得志接口自动化实践

雨墨 玩物少年们
2022年04月28日 02:00


本期内容:

  • 什么是接口自动化测试

  • 框架搭建

  • 工程结构

  • 覆盖率统计

  • 测试报告



自动化测试在当前市场上应用非常广泛,主流有接口自动化测试UI自动化测试

在此基础上,加上持续集成,就能实现全自动化测试。今天主要和大家聊一聊接口自动化测试,以及我们做的事情。

什么是接口自动化测试

目前市场上大部分项目,都是前后端分离的项目,由此产生了大量的接口。而接口自动化测试,主要是对接口进行测试。

顾名思义,接口测试是对系统或组件之间的接口进行测试,主要是校验数据的交换,传递和控制管理过程,以及相互逻辑依赖关系。

在分层测试的“金字塔”模型中,接口测试属于第二层服务集成测试范畴。相比UI层(主要是WEB或APP)自动化测试而言,接口自动化测试收益更大,且容易实现,维护成本相对较低,有着更高的投入产出比。

图片

结合公司目前的业务迭代现状,接口自动化测试也成为了我们进行自动化测试的首选。


框架搭建

我们的自动化测试框架,采用的是“Java+TestNG+Maven”的方式来搭建的。

图片
TestNG

TestNG(Next Generation)是一个测试框架,它受到JUnit和NUnit的启发,而引入了许多新的创新功能,如依赖测试,分组概念,使测试更强大,更容易做到。它旨在涵盖所有类别的测试:单元,功能,端到端,集成等……

TestNG的特点
  • 注解
  • TestNG使用Java和面向对象的功能
  • 支持综合类测试
  • 独立的编译时测试代码和运行时配置/数据信息
  • 灵活的运行时配置
  • 支持依赖测试方法,并行测试,负载测试,局部故障
  • 灵活的插件API -支持多线程测试

Maven

Maven是一个项目构建和管理的工具,提供了帮助管理 构建、文档、报告、依赖、scms、发布、分发的方法。可以方便的编译代码、进行依赖管理、管理二进制库等等。Maven的好处在于可以将项目过程规范化、自动化、高效化以及强大的可扩展性。利用Maven自身及其插件还可以获得代码检查报告、单元测试覆盖率、实现持续集成等等。


工程结构

工程部分: 根据业务接入创建业务module,加一个基础module(包含各个业务数据库操作基类,所有模块的公共类封装)。下面会选取基础和业务模块中比较有代表性的功能进行简要说明。

图片

公用基础模块

维护各业务间通用的公共类、工具类、基础参数封装等。如:登录、获取token等底层基础操作。

1. 通用基础参数封装

手机号、验证码均读取各业务module的applications.properties配置文件

//手机号验证码登录的params
public static JSONObject loginparam(String xxx,String xxx) {
    Header  loginparam = Header.builder()
            .phone(xxx)
            .authCode(xxx)
            .build();
    JSONObject paramMap=new JSONObject();
    paramMap.put("kl_data", loginparam);
    return paramMap;
}
public static String  getxxx() {
    //获取卖家手机号
    String xxx = resourceBundle.getString("xxx");
    return xxx
}

2. 环境分离

我们需要同时在测试环境、预发环境接入自动化工程脚本的执行,所以需要进行环境分离处理。通过启动参数来进行动态获取。

1) 网关路径
//取域名和端口号
String baseurl ;
if(StringUtils.equals(System.getProperty("spring.profiles.xxx"), "pre")){
    baseurl = resourceBundle.getString("pre.url");
}else {
    baseurl = resourceBundle.getString("test.url");
}
2) 测试数据

根据启动参数,匹配获取dev/pre.txt文件

if(StringUtils.equals(System.getProperty("spring.profiles.xxx"),"pre")){
     resource = new ClassPathResource("pre.txt");
     log.info("-----------------------------pre 环境读取");
 }else {
     resource = new ClassPathResource("dev.txt");
     log.info("-----------------------------dev 环境读取");
 }

3. 重试机制

在case的执行过程中,由于网络、发布、断点等原因可能造成case执行失败,增加重试机制

public class RetryToRunCase implements IRetryAnalyzer{
    private static int maxRetryCount = 3;//最大的重跑次数
    private int retryCount = 1;
 
    @Override
    public boolean retry(ITestResult result) {
        if (retryCount <= maxRetryCount) {
            String message = "Running retry for '" + result.getName()
                    + "' on class " + this.getClass().getName() + " Retrying " + retryCount + " times";
            Reporter.setCurrentTestResult(result);
            Reporter.log("RunCount=" + (retryCount + 1));
            retryCount++;
            return true;
        }
        return false;
    }
}

4. 数据库连接及使用

1)配置generatorConfig.xml文件:数据库连接方式,各文件存放位置图片

2)生成文件:使用mybatis-generator插件图片

3)配置dataconfig.xml文件:用于业务module中获取数据库连接方式及各mapper文件图片

独立业务模块

以业务为维度,维护该业务下的公共类、工具类、业务参数封装,以及业务接口case的编写。每个包是以开发工程里的kunlun网关的service名为测试包名,便于后期持续的维护和对照。

1. 业务参数封装

在一个类里面封装多组业务需要的参数,参数组封装需要考虑复用性,降低接口case代码里面的代码冗余

public class BuyerAftersaleParam {
    private static ResourceBundle resourceBundle = ResourceBundle.getBundle("application", Locale.CHINA);
    public static Map<String, Object> ApplyParam(Long xxx,Integer xxx){
        BuyerAftersale applyParam = BuyerAftersale.builder()
                .xxx(xxx)
                .xxx(xxx)
                .build();
        Map ApplyParam = BeanUtil.beanToMap(applyParam);
        return ApplyParam;
    }
}

2. 业务case实现

1) 接口路径

统一配置,各case脚本中直接获取进行拼接。

String uri = "";
//如果传进来的接口名称正确,就返回相应接口的url
if(name == AfterSaleUrl.xxx){
    uri = resourceBundle.getString("xxx.url");
}

2) TestNG注解

上文中有提到TestNG的一个很强大的功能就是注解,注解是一种元数据,能够被脚本编译所识别,起到一定的控制作用。在case的编写过程中,TestNG的注解也给我们提供了很多方便。

  • @BeforeXXX @AfterXXX(Suite/Test/Groups/Class/Method)
  • @DataProvider(将方法标记为测试方法提供数据)
  • @Test(dataProvider、description、enable=false执行忽略此方法、groups对测试方法进行分组、dependsOnXXX依赖、priority优先级、retryAnalyzer重试机制)

3) 断言

我们使用Assert.assertEquals()方法进行断言。

  • 出参断言:断言code和message是最简单的,也可以断言接口response中的其他重要字段。断言code和message可以使用较为灵活的方法,在定义入参时将code和message定义好,在case里,取出response中的code和message与预期的进行比较。

  • 数据断言:通过mybatis进行数据库连接,生成相应的mapper文件,在对应的xml中添加sql,用于查询表中的数据与接口response进行比较。

<select id="selectByOrderId" resultMap="BaseResultMap" parameterType="java.lang.Long" >
    select
    <include refid="Base_Column_List" />
    from aftersale
    where orderId = #{orderId,jdbcType=BIGINT}
</select>

case实例如下:

public class xx extends xx<xx{
 
    private String url ="";
    private String token = "";
 
    @BeforeTest(alwaysRun = true)
    public void BeforeTest() throws Exception {
        setUp();
        //拼接URL
        url= xx;
        //获取token
        token = xx;
    }
 
    @DataProvider(name = "xxx")
    public Object[][] param() {
        return DataUtil.getTestValue("xxx");
    }
 
    @Test(dataProvider = "xxx", description = "xx",priority = 0,groups = "xx",retryAnalyzer = RetryToRunCase.class )
    public void xxxTest(Long xxx,String xxx,int code,String messagethrows Exception 
{
        Map<String, String> headermap = BaseParams.getHeaderWithTokenForBusiness(token);
        JSONObject paramMap = new JSONObject();
        paramMap.put("xxx",xxx);
        paramMap.put("xxx",xxx);
        JSONObject klMap = new JSONObject();
        klMap.put("kl_data",paramMap);
        String httpResponse = HttpUtil.sendPostJson(url, klMap, headermap);
 
        List<xx> xx = mapper.xx(xx);
        System.out.println("test------" + xx.get(0).getReturnReason());
 
        JSONObject response = new JSONObject(httpResponse);
        JSONObject data = new JSONObject(response.get("data"));
 
        Assert.assertEquals(response.get("code"), paramMap.put("code", code).get("code"));
        Assert.assertEquals(response.get("message"), paramMap.put("message", message).get("message"));
        Assert.assertEquals(data.get("description"),xx.get(0).getReturnReason());
    }
}

3. testng文件配置

  • suite声明:用于描述将要运行的测试脚本集,可以根据自己需要任意命名,最终这个名字会在testng的测试报告中看到。
  • groups:如果选择的测试脚本是基于组的(使用了@Test (groups={"student"})这样的注解),那么需要声明如何使用这些组:包含或者排除。使用include标签标注某些组,那么在选择的测试脚本中,只有属于那些组的测试脚本会被运行。那些未被选中的测试脚本,或者被选中却不属于某些组的测试脚本都不会被运行。
  • 选择测试脚本:可以从包(packages)、类(classes)、方法(methods)三个层级进行。
  • 监听器:listener标签用来添加自定义的监听器,例如生成测试报告等。
<?xml version="1.0" encoding="UTF-8"?>
<suite name="测试环境测试脚本集" parallel="null">
    <test name="测试环境测试脚本集"  preserve-order="true">
        <groups>
            <run>
                <include name = "XX0" />    //这里的groups名是跟测试case方法里面注解的group相同,才能执行该case
                <include name = "XX1" />
            </run>
            <packages>
                <package name="com.xxx*"></package>
                <package name="com.xxx*"></package>
            </packages>
        </groups>
        <listeners>
            <listener class-name="org.uncommons.reportng.HTMLReporter" />
            <listener class-name="org.uncommons.reportng.JUnitXMLReporter" />
            <listener class-name="com.xxx.test.xxx.common.TestReport"/>
        </listeners>
    </test>
</suite>
执行结果
图片

以上我们的测试工程已经搭建完成,且以业务维度进行了自动化case的编写及维护。在这些基建工作做完之后,我们还需要一些结果输出来衡量我们的case是否正确且有效,于是我们接入了覆盖率统计及测试报告的自定义优化。


覆盖率统计

设计测试用例的时候,我们要考虑程序的逻辑,每个函数的输入与输出,逻辑分支代码的执行。自动化case的执行情况就需要有一定的数据指标来进行衡量,这个时候我们用代码覆盖率的结果来反向检查case覆盖是否充分完整。同时也可以用来衡量测试工作本身的有效性,提升代码质量、测试效率,减少bug的漏出,以此来提高产品的可靠性及稳定性。

代码覆盖率的意义

  • 分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。
  • 检测出程序中的废代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑关系,提升代码质量。
  • 代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量不会高到哪里去,可以作为测试自我审视的重要工具之一。

我们选用基于jacoco的方式来进行覆盖率统计。jacoco是一个开源的代码覆盖率工具,针对java语言,其使用方法很灵活,可以嵌入到Ant、Maven中。

Jacoco从多种角度对代码进行了分析:

  • 指令级覆盖(Instructions,C0coverage):计数单元是单个java二进制代码指令,指令覆盖提供了代码是否被执行的信息,度量完全独立源码格式。
  • 分支(Branches,C1coverage):度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的分支数量。
  • 圈复杂度(CyclomaticComplexity):在组合中,计算在一个方法里面所有可能路径最小数目,缺失的复杂度同样表示测试用例没有完全覆盖到这个模块。
  • 行覆盖(Lines):度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。
  • 方法覆盖(non-abstract methods):度量被测试程序的方法执行情况,是否执行取决于方法中是否至少有一个指令被执行。

jacoco是通过修改class文件的字节码来进行代码覆盖率统计的。即,在原有class字节码中的指定位置插入探针字节码,形成新的字节码指令流。jacoco的探针实际是一个布尔值,当代码执行到探针位置时,将其置为true,该探针前面的代码会被认为执行过。

jacoco的插桩(插入探针)模式有on-the-fly和offline两种,我们全量采用offline的方式统计。

offline模式:编译时插桩,在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试插过桩 的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。

大致流程如下:

  1. 后端服务启动加一个 javaagent 参数,相关jacoco参数如下:javaagent:/home/jmsmanager/jacoco/lib/jacocoagent.jar*,output=tcpserver,address=xxxxx,port=xxxxx
  • javaagent配置:jacocoagent.jar所在目录
  • output配置:输出类型,默认tcpserver
  • address配置:指定业务服务的IP地址
  • port配置:端口号,指定javaagent的端口,可随意指定,不冲突即可
  1. 测试代码可以通过网关指向单台业务机器,保证每次接口接口测试用例能全覆盖

  2. 通过ant dump拉取指定的机器的exec文件,再通过ant report 得到测试报告数据

流程图如下:图片


结果输出如下:

图片

除上述的覆盖率统计外,我们还接入了有效接口的覆盖率统计。

有效接口的范围定义为,grafana上统计到有调用量的接口,且为App端调用接口。将我们case覆盖到的接口list与拉到的有效接口list进行对照,输出有效接口的覆盖率。图片

测试报告

TestNG自带的测试报告在美观度、功能自定义上相对较差,我们便选用了ReportNG来自定义生成测试报告。可以更加清晰的看到case的执行情况,失败、跳过、成功数量,及耗时等数据。

在testng.xml中添加listener:图片

报告输出结果如下:图片

图片

日常使用

我们做接口自动化的目的还在于,提升测试工作效率,减少冗余投入,保障业务质量。在自动化case维护到一定的数量之后,我们将接口自动化执行接入到了发布系统,在每次代码发布之后自动执行,并将结果发送至钉钉群内,便于测试和开发同学更加直观、及时的查看结果,发现问题,解决问题。

图片

以上就是目前我们已经做的一些事情,还有很多需要继续完善、优化、提升的部分,我们也在持续不断的学习当中~



继续滑动看下一个
玩物少年们
向上滑动看下一个