以前一直都是在 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 自动化框架和单元测试的框架以后再分享。


↙↙↙阅读原文可查看相关链接,并与作者交流