腾讯WeTest/专栏文章/测试开发之路----可读性,可维护性,可扩展性 (续)/

测试开发之路----可读性,可维护性,可扩展性 (续)

开发测试干货
测试开发 2995

前言

测试开发之路----可读性,可维护性,可扩展性结尾的比较着急,有一个很常用的设计没有讲,就是可插拔式设计,下面我们来一步一步实现这个机制。

为了变化,将算法抽象出来

举个常见的例子,假如我们现在有一个接口测试框架,它是测试http协议的接口,一切工作的很正常。但是如果突然某一天开发那边的架构变化了,为了负载均衡他们将模块分离,并用RPC协议作模块间的沟通。 于是乎一个基于RPC协议的java接口测试需求出现了。即便不是RPC协议接口。普通的java接口测试需求也有很多有获取方式,有用spring管理的,有单纯用Jar包的,也有webservice的。我们不可能一次性的把所有的可能性都想到。只能是碰到的时候再实现。但是我们预料到了之后的变化,就需要留下一种策略能让框架自由的选择哪一种方式的同时,保证可读性,可维护性和可扩展性。这时候千万别if else起来没完,我上一章已经讲过了大堆if else和for 循环嵌套的弊端了。

所以这时候我们决定把每种接口的获取方式都看成一个算法。这些算法有统一的接口,其他模块调用的时候,是 面对这个接口编程 的,这个思想很重要。现在我们定义这么一个接口。

/**
 * 工厂接口:工厂方法返回的是RD待测接口的对象
 * 目前的调用方式为:spring,jar包,http接口。所以预计有三个具体工厂类。如果以后出现新的调用方式,实现此接口即可
 * 具体工厂类一定要穿件在:com.bj58.daojia.test.InterfaceTool.data.classFactory这个包中。
 * 
 * @author Gaofei Sun
 *
 */
public interface IInvokeObjectFactory {
    public Object getInvokeObject(String type);
}
}

这个接口是所有算法的基类,调用方只知道这个接口,针对这个接口编程。每个字类其实都是个工厂类,责任就是给框架创建被测的接口对象,根据不同的创建方式有不同的实现,我们把每个实现都看作一个算法。下面我们看看通过Spring 管理dubbo协议的实现

/**
 * 具体工厂类:使用spring获取待测对象
 * 
 * @author Gaofei Sun
 *
 */
@Component
public class Spring implements IInvokeObjectFactory {
    public static String zkAddress = "zookeeper://192.168.120.88:2181";
    // BeanFactory beanFactory = null;

    public Spring() {
        // beanFactory = new ClassPathXmlApplicationContext("./spring-dubbo.xml");
    }

    @Override
    public Object getInvokeObject(String type) {
        try {
            System.out.println();
            Class temp = Class.forName(type);
            return getInvokeObject("daojia_services_interface", temp);
        } catch (Exception e) {
            e.printStackTrace();
            Assert.assertTrue("Sping获取目标对象失败,请检对象名称是否正确,或配置文件是否正确。传入的对象名:" + type, false);
        }
        return null;
    }
private static Object getInvokeObject(String group, Class interfaceClass) {
        try {
            ApplicationConfig application = new ApplicationConfig();
            application.setName("dj-card-client");

            RegistryConfig registry = new RegistryConfig();
            registry.setAddress(zkAddress);
            registry.setGroup(group);
            // System.out.println("registry build completed.");

            ReferenceConfig reference = new ReferenceConfig();
            reference.setApplication(application);
            reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
            reference.setInterface(interfaceClass);
            reference.setVersion("1.0.0");
            reference.setCheck(false);
            // System.out.println("reference build completed");
            Object obj = null;
            try {
                obj = reference.get();
            } catch (Exception e) {
                System.out.println("error:" + e.getMessage());
            } finally {
                // System.out.println("finally." + "houseLeaveService:");
            }
            return obj;
        } catch (Exception e) {
            e.printStackTrace();
            Assert.assertTrue("Sping获取目标对象失败,请检对象名称是否正确,或配置文件是否正确。传入的对象名:" + interfaceClass.toString(), false);
        }
        return null;
    }

上面是我们当初通过Spring 去获取dubbo协议的对象的实现,可以看到代码不算少吧。我们还有HTTP的协议,SCF协议(公司自主研发的协议),SF协议(同样是公司自主研发的),每一种协议都需要一些代码去获取对象, 我们想象一下每次用if else 管理的话得多乱.以后每次来换新的协议你都跑来改代码,加个if 分支也是挺痛苦的 . 我们有了接口,有了实现. 现在我们再用一个工厂类来生产这些算法。

/**
 * 创建带测接口对象工厂的工厂类 
 * @author Gaofei Sun
 *
 */
public class InvokeObjectFactoryFactory {

    private static ConcurrentHashMap<String, IInvokeObjectFactory> map = new ConcurrentHashMap<String, IInvokeObjectFactory>();

    public static IInvokeObjectFactory getInvokeObjectFactory(String type) {
        String temp = convertStringTo1lower(type);
        return (IInvokeObjectFactory) SpringContext.getBean(temp);
    }

上面的代码很简单,由于我后来把对象都交给spring管理了,所以我们创建算法对象的话,连if else也不用了。直接传一个name进来用spring来为客户端创建算法。于是乎客户端就变成了这样。

// 获取待测接口对象的工厂长对象
IInvokeObjectFactory factory = InvokeObjectFactoryFactory.getInvokeObjectFactory(info.getInvokeType());
Object invokeObject = factory.getInvokeObject(info.getClassName());

OK,看到了吧,客户端不清楚算法具体怎么实现的,也不知道是在调用哪个算法,反正我只需要知道这些算法的接口就行了。 传递一个 算法名称 就可以创建算法了,而算法名称其实也是外部传递进来的。所以实际上客户端除了这个接口以外,什么都不需要知道(知道的最少原则)。以上的例子是使用了spring的bean工厂, 工厂模式 和 策略模式(如果没有大家没有使用spring的话,自己写享元模式或者用java反射也是可以的)。想想看这是不是比用if else管理大篇的代码方便多了。扩展起来也很容易。不必修改原来的代码。只需要新加一个算法就可以。 符合面向对象的 开闭原则 。

可插拔式设计

用过testng的或者tomcat的人一定对这种设计不陌生,tomcat的配置文件里准许用户自定义connector然后注册进tomcat里代替默认的。testng也可以自己实现监听器然后注册进testng中用以定制化report。据我所知QTP的关键字驱动框架中也有自定义关键字的的功能(没用过,听同事说的)。下面看一下testng的例子。

<listeners>
        <listener class-name="InterfaceTool.testListener.ReporterListener"></listener>
        <listener class-name="InterfaceTool.testListener.TestListener"></listener>
        <listener class-name="InterfaceTool.testListener.SuiteListener"></listener>
        <!--  
        <listener class-name="com.bj58.daojia.test.InterfaceTool.testListener.RetryListener"></listener>
        -->
</listeners>

可以看到上面就是testng的可插拔机制。我自己实现一个listener然后注册进来。这样testng使用的就是我的ReporterListener生成测试报告了。这些都是可插拔式的设计,基于控制反转的思想,我们定义了好了行为,控制实现留给用户扩展。 有时候这种设计可能很有必要,因为可能你写的框架在一个公司里有很多业务线在用,你无法预测出所有的情况,所以干脆留个接口让他们扩展。或者你也有关键字驱动的框架,你需要能让用户有能力自定义自己的关键字。所以这种设计就变得很有必要。下面我们说说具体的实现原理。通过帖子最开始的那个例子,我们现在已经把算法抽象出来了,留下了一个接口。所有的算法都实现这个接口,我们的客户端也是面向这个接口编程的,也就是说,你是用什么算法我不关心,只要你的算法实现了这个接口就行了。也有一个专门的工厂类来帮客户端创建这些算法。如果像tomcat或者是testng这种算法是唯一的,也就是只能使用一种算法。那么其实这个工厂类也就不用参数了。如果是关键字驱动这种有很多算法的,我们创建算法的工厂类就需要一个name来做参数,判断创建哪个算法。然后我们在运行时读取测试人员编写的用例中使用的哪种关键字当作参数就可以了。例如下面的例子:

<methods url="api/model/deleteModel" httptype="post" invokeType="http" verifyMethod="">

可以看到上面的xml文件就是测试人员要填写的。中间的那个invokeType 就是说我用哪种方式获取接口。这种 把决定使用哪种实现的权利交给用户的机制,很好的体现了控制反转的思想,同时我们编程的时候也完全不用考虑到底是在用哪个算法,这样就充分的解耦了。那么现在我们其实还需要一种注册方式,把用户自己定义的方法注册进框架里。如果测试框架和项目都使用spring管理的话就简单了,像第一个例子一样,直接交给bean工厂管理就行了。写算法的时候一个@component注解就行了。如果大家不是用spring管理的,那么就需要使用 享元模式 加java反射来搞这个事情了。 首先我们搞一个XML的配置文件,定一个标签让用户去注册算法,就像上面testng的做法一样。然后我们在帖子第一个例子中的创建算法的工厂类中,就要像下面这么写(其实就是个 享元模式 )

public class InvokeObjectFactoryFactory {

    private static ConcurrentHashMap<String, IInvokeObjectFactory> map = new ConcurrentHashMap<String, IInvokeObjectFactory>();

    public static IInvokeObjectFactory getInvokeObjectFactory(String path,String name)

        if (map.containsKey(name)) {
            return map.get(name);
        } else {
            IInvokeObjectFactory factory = null;
            try {
                factory = (IInvokeObjectFactory) Tools
                        .reflectObject(path);
                map.putIfAbsent(factory , name);
            } catch (Exception e) {
                e.printStackTrace();
                Assert.assertTrue("无法生成对应的调用方式:" + path+ " 请检查是否调用方式输入错误", false);
            }

            return factory;
        }
    }

我们看到通过一个name和一个自己实现的算法的路径为参数。 是用java反射的方式创建这个路径的算法,然后用name为key存到一个map中。以后其他模块调用算法的时候,就会去这个map里获取算法了。这样我们就把自定义的算法注册到了框架里。

总结

我们来说说这样做的好处。其实最大的好处就是 扩展性 和 维护性 强,以后新加算法的时候我们随便在哪实现预先定义的算法接口然后注册进框架就行了。 我们不需要改动哪怕一行原来的框架代码。相比我们用if else去hold住所有情况的方式,加一个算法你就要跑去框架里相应的位置去加一段if 分支的方式实在是痛苦之极,尤其是你需要对你原来的框架作不小的改动。而这个改动可能只适应你公司的一条业务线,不同的业务可能需要不同的算法。而每出现一个新算法业务线的人都跑来让你加进去你说你烦不烦。举个例子你是做关键字驱动框架的, 那么每个关键字都必然是你来写了,因为每个测试人员想要定义一个新关键字的时候都跑来找你写(他们自己写不了,框架代码他们也看不懂,就算他们知道了在哪里加if分支在哪里加算法,你敢让别人随便动你的框架么?),而且这种一换项目就会废弃掉的算法在你的框架里呆着也是不妥的。假如你跳槽了,你说这些算法你是删还是不删。还是退一万步讲,一旦你离开了这个岗位。接手这个框架的人真得通读一遍框架代码才知道怎么扩展关键字了。如果假如这个框架还没文档没注释的。。。那他会不会疯掉。。。

所以我十分鼓励设计可插拔式的框架,准许用户自定义实现他们想做的事情。
注意:文章中所有的设计方式不仅仅在关键字驱动中才有用。很多地方你都可以用到它 。



原文发表于:Testerhome;

作者:ycwdaaaa ; 

原文链接:https://testerhome.com/topics/4884



目前腾讯WeTest服务器性能测试已经正式对外开放业务场景模拟,持续压力触达服务器极限,帮助寻找服务器性能问题!点击链接:http://wetest.qq.com/gaps/立即体验!

热度排行榜

购买
客服
反馈