接口测试 NO_CODE 接口自动化测试框架

孙高飞 · 2016年04月29日 · 最后由 looshing 回复于 2018年10月29日 · 4217 次阅读

以前一直都是在 TesterHome 上看文章,也没有想过发帖子,因为我不是做移动端测试的。最近也是看了大家有很多做接口测试的分享 ,自己也有一些想法,所以发上来跟大家分享一下。
我刚来现在的公司 1 个多月,目前只有我这么一个做测试开发的。之后会招懂代码的帮我,我定的计划暂时就是 8 个字:分层测试,持续集成。自动化环境部署直接用开发弄得 docker 镜像。现在也就是在 UI 自动化,接口自动化和单元测试自动化上下功夫。这一个多月光写测试框架了,接口测试的用例只追了 70 多条。之后招人做 UI 自动化和接口自动化,开发会在我搞的测试框架上做单元测试。
好了,背景介绍完毕。现在开始说说接口测试的事。公司有 HTTP 接口和 dubbo 接口,开发语言是 java,所以想调用 dubbo 接口只能用 java。我也只能用 java 写了一套框架。
由于暂时会写代码的比较少,所以我做两套框架,一套要写代码,比较灵活。 一套不用写代码,但是不灵活。
先说如何调用 dubbo 接口吧,dubbo 接口说白了,就是个 RPC 协议的 java 方法。为了不让手工测试的人写代码,我选择的是定义了一套 xml 标签规定我需要的信息,然后使用 java 反射调用接口,并判断返回值和数据库中的数据。直接上例子

<methods methodName="addPurposedNurse" invokeType="Spring" className="com.bj58.daojia.ordercenter.agent.house.NurseOrderService"> 
    <params alias="测试正常情况" dataFile="addPurposedNurse.xls"> 
        <in name = "HouseSellerDto" type="Object" path="com.bj58.daojia.ordercenter.dto.house.HouseSellerDto">
            <subP name="sellerId" type="Long">22332112345670</subP>
            <subP name="realName" type="String">保姆1</subP>
            <subP name="phone" type="Long">18701484085</subP>
        </in>
        <in name="orderId" type="long">12332112345670</in>


        <out type="Object" name ="OperationResult" path="com.bj58.daojia.ordercenter.dto.support.OperationResult">
            <subP type ="Boolean" name="result">true</subP>

            <subP name="message" type="Map" ifverify="false">
                <subP name="key" type="Map">
                    <subP name="key" type="Integer">0</subP>
                </subP>
                <subP name="value" type="Map">
                    <subP name="value" type="String">正常</subP>
                </subP>
            </subP>
        </out>

        <database database_name="dbwww58com_emclottery">
            <table table_name="t_app_order_paidan" where="orderid=12332112345670">
                    <row>
                        <field field_name="orderid">12332112345670</field>
                        <field field_name="sid">22332112345670</field>
                        <field field_name="stel">18701484085</field>
                        <field field_name="sname">保姆1</field>
                    </row>
            </table>
        </database>
    </params>

上面的例子里,就是测试人员填写调用接口的必要信息,例如 java 类的路径名称,方法名称,入参和返回值等。后台自然就利用 java 反射的机制调用接口。这个框架需要注意的其实就是参数的类型转换和返回值的验证了。由于这是 java 接口,而不是 http 协议那么简单,java 是一种强类型语言。从 xml 文件读取的数据都是 string 类型。需要做相应的类型转换。所以在 xml 文件中每个参数(不管是输入参数--in 还是返回值--out)都有一个 “type” 属性,框架中有相应算法做类型转换。 而由于 java 中的参数也可以是很复杂的,是一种树形结构。例如一个 list 里面装的是 javaBean,而 javaBean 中又有一个 list 类型的属性等。所以读取 xml 文件的时候就需要递归的遍历整个树结果,然后同时也是递归调用相应的类型转换算法加以转型。我说点实现细节吧

上面就是目前所有的转型算法,抱歉媳妇的电脑没有 IDE,我只能截这么个图了。所有的算法实现 TypeConvert 这个接口,通过策略模式调用算法。内部外部都通过 TypeConvertFactroy 这个工厂类去创建转型算法对象。

public class TypeConvertFactroy {
    // private static ConcurrentHashMap<String, TypeConvert> map = new
    // ConcurrentHashMap<String, TypeConvert>();

    public static TypeConvert createTypeConvert(String type) {
        if (type == null || type.equals("")) {
            type = "String";
        }
        String beanName = Tools.convertString(type) + "Convert";
        return (TypeConvert) SpringContext.getBean(beanName);

        /*
         * if (map.containsKey(type)) { return map.get(type); } else {
         * TypeConvert obj = null; try { type =
         * com.bj58.daojia.test.InterfaceTool
         * .data.paramLoader.typeConvert.Tools.convertString(type); obj =
         * (TypeConvert) Tools .reflectObject(
         * "com.bj58.daojia.test.InterfaceTool.data.paramLoader.typeConvert." +
         * type + "Convert"); map.putIfAbsent(type, obj); } catch (Exception e)
         * { e.printStackTrace(); Assert.assertTrue("没有找到 " + type +
         * " 的参数类型,请核对是否输入错误的参数类型或请在系统中增加对应的参数类型", false); }
         * 
         * return obj; }
         */

    }

}

工厂方法里也是用 spring 的 bean 工厂管理去创建对象的。 本来之前没使用 spring 的时候是用的被注释的那段 java 反射代码去生成对象的 (本人很喜欢 java 反射)。 这么设计的原因是为了可扩展性。以后加新的转型算法的时候,只要在特定的路径下创建复合命名规则的算法类就可以了。而不必加入新的代码。所有的算法存在内存中,使用表驱动的方式,存在一个线程安全的 map 中:ConcurrentHashMap 以防以后多线程环境的调用。

验证返回值的机制也是类似的。如下图。

每一种类型的验证都对应一个验证算法。 不过与入参不一样是,无法使用表驱动的方式去获取算法对象,因为你无法用一个简单的方式来判断当前的返回值该由哪个算法去处理。尤其是 javaBean 类型,根本没有任何办法判断当前对象是一个 javaBean。所以我使用责任链模式组织这些算法。如下图,这个单例的工厂类负责组装责任链

public class VerifyHandlerFatory {
    public static void main(String[] args) {
        System.out.println(VerifyHandlerFatory.createVerifyHandler());
    }

    private VerifyHandlerFatory() {
    }

    public static VerifyHandler createVerifyHandler() {
        return VerifyHandlerHolder.first;
    }

    public static class VerifyHandlerHolder {
        private static VerifyHandler first = createVerifyHandler();

        /**
         * 组装验证责任链,并返回第一个节点的对象
         * 
         * @return
         */
        public static VerifyHandler createVerifyHandler() {
            // 第一个节点为基本数据类型,这个类型的参数最多
            first = new PrimitiveType();
            VerifyHandler ListType = new ListType();
            VerifyHandler mapType = new MapType();
            VerifyHandler jSONObjectType = new JSONObjectType();
            VerifyHandler enumType = new EnumType();
            VerifyHandler collectionType = new CollectionType();

            // 最后一个节点为Object:因为无法判断具体的Object类型,所以放在最后一层,不属于其他类型的就是Object
            VerifyHandler ObjectType = new ObjectType();

            // 开始组装责任链
            first.setHandler(ListType);
            ListType.setHandler(mapType);
            mapType.setHandler(jSONObjectType);
            jSONObjectType.setHandler(enumType);
            enumType.setHandler(collectionType);
            collectionType.setHandler(ObjectType);
            return first;
        }
    }
}

具体原理就是把期望的对象和实际的返回值传递给链表的第一个节点。第一个节点判断当前对象类型是不是自己应该处理的。如果是,就处理,不是就传给下一个节点判断,一次类推,一直到最后一个节点--javaBean 类型。由于无法判断对象是否是 javaBean 类型,所以其他所有节点都判断不是自己该处理的类型了,那说明就是 javaBean 类型。下面贴一段基本类型的验证代码。

public class PrimitiveType extends VerifyHandler {

    @Override
    public Boolean PassRequest(Object actualValue, Object expectedValue, String fieldName_no) {
        if (expectedValue.getClass().isPrimitive() || expectedValue.getClass().isAssignableFrom(String.class)
                || expectedValue.getClass().isAssignableFrom(Boolean.class)
                || expectedValue.getClass().isAssignableFrom(Integer.class)
                || expectedValue.getClass().isAssignableFrom(Double.class)
                || expectedValue.getClass().isAssignableFrom(Float.class)
                || expectedValue.getClass().isAssignableFrom(Short.class)
                || expectedValue.getClass().isAssignableFrom(Byte.class)
                || expectedValue.getClass().isAssignableFrom(Long.class)
                || expectedValue.getClass().isAssignableFrom(Float.class)
                || expectedValue.getClass().isAssignableFrom(Date.class)) {


            Assert.assertEquals("返回值与预期值不符:" + fieldName_no, expectedValue, actualValue);
            return true;
        } else {
            return nextHandler.PassRequest(actualValue, expectedValue, fieldName_no);
        }
    }
}

到此这个框架中最难处理的部分就说完了。虽然很笼统,但是就说个大概的思路,有个思路大家就知道怎么回事了。但是现在还有一点需要说明的是,说到现在都只是在说接口调用的问题。还有一个问题也很重要,那就是测试数据的准备和销毁问题。 我看到很多其他人的框架,不论是 http 接口的还是 RPC 协议的。都是在模拟调用,判断返回值。但是都没有说到测试数据的准备和销毁。 我们都知道,测试数据决定了我们的测试用例是否能重复执行。只有重复执行了,这个才算是真正的自动化。 我看很多人做的接口自动化,实际上根本就没有自动化,因为他需要测试人员手工准备测试数据。 我不知道其他公司是怎么解决这个问题的。我解决的方式很简单粗暴。 框架直接提供一种机制直接往数据库里插入测试数据,再由框架负责销毁他们。还记得 xml 里面有一个 dataFile 的标签属性么?这个 dataFile 就是一个 excel 文件,里面的行和列就对应着数据库的行和列,xml 文件指定数据文件的路径,框架负责读取这个 excel 文件,拼接出 insert 和 delete 语句。为数据库执行创建和销毁操作。文件例子如下:

第一行是库和表的名字,以下的就是数据信息。 实际上这个文件可以用很多工具自动生成出来。我们用的 navicate for mysql 就可以导出 excel 格式的文件。所以我们的思路都是第一次在 UI 上创建测试数据,然后导出 excel 文件,稍作修改便用在自动化上了。也满方便的。用例执行前创建数据,用例结束后,销毁数据。 当然如果被调用的接口内部创建出的数据就无法删除了。 所以我在读取 excel 文件的代码里加了一段逻辑。 凡是 delete 开头的 sheet 中的数据只删除 ,不创建。 这样可以定义销毁接口本身创建的数据。

哦, 对了, 还有一个比较关键的是,我看很多人做的接口测试框架只验证返回值,而不去管数据库中的记录。我觉得这也是验证不完全的。所以我在 xml 里还定义了一套标签是这样的:

<database database_name="dbwww58com_emclottery">
            <table table_name="t_app_order_paidan" where="orderid=12332112345670">
                    <row>
                        <field field_name="orderid">12332112345670</field>
                        <field field_name="sid">22332112345670</field>
                        <field field_name="stel">18701484085</field>
                        <field field_name="sname">保姆1</field>
                    </row>
            </table>
        </database>

这套标签专门拼 sql 验证数据库。

好了,这套框架的 XML 版本的主要思路就介绍到这了。当然还有很多细枝末节,例如如何获取 dubbo 协议的对象,如何数据驱动等就不一一介绍了。 这个框架的问题在与,这始终还是 java 接口。如果使用框架的人对 java 一无所知的话,仍然很难使用。例如他可能根本就不知道什么是 List 什么是 javaBean。0 基础的人还是很难做的下去。再一个很复杂的接口在 XML 中的定义也会很复杂。不如直接写代码来的方便。 这是个问题, 所以我这里也是有一个需要写代码的框架。其实跟这个功能一样的。只不过是直接在代码里调用这些个 xml 里的功能。以后会写代码的测试多了,也许就会用那个版本的框架了吧。

好了,今天先说到这吧。关于 UI 自动化框架和单元测试的框架以后再分享。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 41 条回复 时间 点赞

你好,可以贴下 ListConvert 怎么实现的吗?

孙高飞 测试开发之路 ---- 数据驱动及其变种 中提及了此贴 12月10日 11:45

dubbo 用 socket 去处理就行

—— 来自 TesterHome 官方 安卓客户端

自己做这套东西的初衷是什么?市面上没有满足尼需求的开源工具么

很不错的想法,也继续鼓励在社区发帖子~

很不错,毕竟要给不会代码的测试用

—— 来自 TesterHome 官方 安卓客户端

#1 楼 @doctorq 我就是用 testng 驱动的。不是说一点开源框架不用的。

#2 楼 @monkey 多谢支持

#4 楼 @sordar 但还是感觉不会写代码的人来做这种接口的测试比较困难。我在考虑要不要不这么弄

由于暂时会写代码的比较少,所以我做两套框架,一套要写代码,比较灵活。 一套不用写代码,但是不灵活。
--写代码和不写代码为什么要做两套框架,这两者并不互斥!

#8 楼 @quqing 基本是一套。但是两种形式。一个写代码一个不写代码。在领导那看。就跟两套一样

不错的,我们也在做 HTTP 接口和 dubbo 接口自动化

#10 楼 @success 呃,我有个疑惑,dubbo 接口自动化只能通过读取 xml 文件,java 反射的方式去动态生成测试接口了么?

#11 楼 @sigma 不是啊。写代码显示的调用是最常用的。不过我们当时没几个会写代码的。所以我就搞成这样给他们用了

#11 楼 @sigma 他这里应该是给那些不用写代码,只写些配置文件的人用的吧。当然,你也可以全部写代码去调用。每个公司都有自己的要求。关键还是看领导喜欢什么模式,哈哈

#12 楼 @ycwdaaaa 我之前写过一个特别简单的小程序,RPC 的,我这边需要拿到研发的接口代码啥的,然后都注册到 zk 上,之后调用,你那个我看着很复杂(高大上),相当于你定义了文件格式,然后动态地去生成这些接口的 java 文件作为 client 端么

#13 楼 @success 哈哈,领导要是喜欢看不停干活不停敲代码的,那我就直接每个接口写个 testmethod,不做啥数据驱动的框架之类的~

#12 楼 @ycwdaaaa 还有个问题,你最后那个 check db 的 xml,是针对于本次测试所有需要校验的字段写成一个大的 xml,里面包含了很多子节点,然后拼接 select orderid,xxx, xxx,xxx from t_app_order_paidan where orderid=12332112345670 执行 sql 后,再根据标签中的值做验证么?支持那种可随意配置需要验证 db 和不需要验证 db 么(比如我去指定目录去遍历解析这个 xml,没找到就不做验证)?

#16 楼 @sigma 是这样的,找不到就不验证。找到了就验证

#19 楼 @ycwdaaaa 哈哈,关键我就差在这些设计思想与形式上,有些东西自己有可能想半天或者实验半天都没有很好的效果(但是绝对不会自己不思考),看看其他人的文章,说不定就通了,而且我比较初级,先从模仿开始喽~

#17 楼 @ycwdaaaa 哈哈,读了你的文章真是对我启发很大,非常感谢你耐心的回复~我也准备自己实现下试试~~期待你之后的系列文章~

#18 楼 @sigma 别期望太大~~其实没多少内容。这些其实只是没人整理一下。其实明白了形式。你研究研究就都实现了。

有个问题请教下,你有用类似 orm 的框架吗,用的是哪个?
还是你是直接用 sql 语句的啊?

#21 楼 @sziitash 我用的 mybatis

现在自己很菜,好多不理解,但对楼主写的这些比较感兴趣,请问楼主有没有开博客,想多了解些

#23 楼 @xiaobaicai123 暂时还没有博客~

给手工测试的人使用,需要编辑 xml 文件吗?我很久以前做过一个小工具,使用 xml 来输入数据和一些简单配置。比楼主上面演示的例子要简单的多。但是一点不懂开发的人还是很抗拒通过编辑 xml 文件来使用小工具。最后是做了一个图形界面来间接编辑 xml。

#25 楼 @frankliu 凡是抗拒的一律镇压哈哈哈, 开玩笑啦. 后来写了个能自动生成 xml 的工具

#25 楼 @frankliu 隐约看到自动化测试平台的影子哇~~

#26 楼 @ycwdaaaa 你这篇文章给了我很大的启发,尤其是最后的落地数据校验,我仿照你那个 xml 写了个 json 版的,但是发现数据库字段读取出来 (Java & mysql),比如 Date 类型,给我自动带了个.0 出来,就像 2011-09-09 09:09:30.0 这个样子,不知你有没有遇到过类似的问题,或者需要自己实现一个 convert 支持各种数据库字段类型的转换?

#28 楼 @sigma 我是专门判断是不是 data 类型,如果是就把最后那个 0 去掉

@ycwdaaaa ,其实比较返回值也可以用 SpringContext.getBean(type + "Verfier") 这种方式找到某个 Verfier,然后用 Verfier.verify 去比较结果,这样做有 2 个好处
1 添加新类型只要加一个文件,不像责任链还要把这个新类型串入责任链
2 不但验证了返回值还验证了返回类型。
你觉得呢

孙高飞 [该话题已被删除] 中提及了此贴 06月28日 18:51
孙高飞 [该话题已被删除] 中提及了此贴 06月28日 18:51

你好,能留个联系方式聊下么?

#33 楼 @lilithlovesyou 446051551 是我 qq

#3 楼 @taki
具体怎么调用呢,能给个 DEMO 么

其实第二种框架对于代码的要求并不高,如果是公司内部使用,培训几次,写一些简单的接口测试类并不难,只要框架封装好

—— 来自 TesterHome 官方 安卓客户端

定义销毁接口本身创建的数据这个具体是怎么做的呢,目前我这样做只能去看开发逻辑才能去删除响应数据

#37 楼 @tcat 你可以看看我写的监控式数据管理方式,我通过 assertJ 的 changes 概念写了个工具。 自动对比用例执行前和执行后数据库中数据的变化并拼出 sql 语句,将数据恢复回去。

孙高飞 测试开发之路 -- 持续集成 中提及了此贴 11月23日 08:52
孙高飞 测试开发之路 ---- 框架中数据的管理策略 中提及了此贴 12月02日 10:47

自动化的难点就在维护 和数据的处理上,对于我们这数据库只能查询权限来说,没有好的办法,只能通过 sql 查询想要数据。

写得真好,赞一个!

@ycwdaaaa 楼主,最近我要写 Dubbo 接口的自动化测试用例,想测试接口里面的逻辑,不知道如何搭建框架,可以给一些提示吗?

ABEE ycwdaaaa (孙高飞) 在 TesterHome 的发帖整理 中提及了此贴 01月12日 13:47
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册