在我们最初学习和使用selenium进行自动化的时候,肯定都是在本地IDE中进行脚本编写和执行脚本。最常用的执行方式就是使用单元测试框架,如java的testng,ruby的rspec,python的unittest。当我们在IDE中执行的时候其实是多个用例串行执行的,如果进行单元测试,其实这样做是无所谓的。毕竟代码级别的执行效率是很快的。再多的用例很快也能执行完成。但是如果是执行UI自动化脚本,当用例数几百上千后会非常慢,而且在一台机器上执行很有可能因为硬件和网络问题而运行失败。所以我们要进行分布式执行,最常用的方式就是通过jenkins或者selenium grid来执行。但这样做仍然觉得使用起来不够直观清晰。我们也可以通过消息队列来实现分布式的脚本执行。我选用的redis,其他的MQ工具也是可以的。

redis实质上是一个内存数据库,做数据存储的。但是在自动化测试中,我们可以用它的订阅发布来实现消息的传递。当我们要执行一轮测试的时候,就向一个频道发布消息。而脚本执行客户端订阅这个频道,并接受到我的发布信息。当客户端接收到信息后,就根据这个信息去redis服务器读脚本的执行队列。在队列中取到每个客户端需要执行的脚本名称,然后就开始执行这个脚本。执行完后再去队列中取下一个脚本名称,依次循环直到队列为空。基本的设计原理就是这样。

先封装redis的方法,我使用的是java jfinal框架,Redis.use().getJedis()是该框架获取jedis对象的方法:

public class RedisKit {
	
	public void pubMesage(String channel,String message) {
		Jedis jds = null;
		try{
			jds=Redis.use().getJedis();          //获取jedis对象
			jds.publish(channel, message);        //publish 发布消息
			System.out.println("测试任务队列id:"+message+" 发布成功!");
		}catch (Exception e) {
			System.out.println("测试任务消息发布失败:"+e);
		}finally {
			jds.close();
		}
	}
	
	/**
	 * 添加队列元素
	 * @param queName   队列名称
	 * @param el      加入的元素
	 * @return
	 */
	public boolean addQueueEle(String queName,String el) {
		Jedis jds = null;
		boolean isCreate = true;
		try{
			jds=Redis.use().getJedis();
			jds.rpush(queName, el);          //rpush将元素插入队列
			System.out.println("脚本:"+el+" 插入队列 "+queName+" 成功!");
		}catch (Exception e) {
			isCreate = false;
			System.out.println("脚本名加入测试任务队列 "+queName+" 失败!");
		}finally {
			jds.close();
		}
		return isCreate;
	}

}

在service类中调用上面的方法:

public String addTaskQueue(String pid, String cli, String rid) {
	List<Runinfo> tasks = null;
	boolean isCreated = false;

	RedisKit rkit = new RedisKit();
	HashMap<Object, Object> result = getCaseWaitRun(pid, rid);
	if (result.get("status") == Status.SUCCESS) {
		tasks = (List<Runinfo>) result.get("body");
	}
	isCreated=createQueue(rid, tasks);      //round id 作为队列名称

	// 创建队列成功,就发布消息
	if (isCreated == true) {
		rkit.pubMesage(cli, rid);
	}
		
	return Status.booleanResultCode(isCreated)
}

/**
   * 创建脚本执行队列
   * @param queueName   队列名称
   * @param els       要加入的元素,脚本名称
   * @return
*/
public boolean createQueue(String queueName, List<Runinfo> els) {
	boolean stQueue = false;
	RedisKit rkit = new RedisKit();
	for (int i = 0; i < els.size(); i++) {
		String queueElement = els.get(i).get("caseName");
		stQueue = rkit.addQueueEle(queueName, queueElement);
	}
	return stQueue;	
}

在controller中接收前端参数,并返回执行结果:

/**
	 * 接收到前端传递的round名,启动该round的测试,发布测试消息到客户端,
	 * 并将round中的case名加入测试任务队列
	 */
	@ActionKey("/startRun")
	public void startRun() {
		String pid=getPara("pid");           //项目id
		String rid=getPara("rid");           //本轮测试id
		String client=getPara("cli");        //执行客户端地址
		
        //如果客户端地址为空,则表示所有客户端都可以执行,所以赋值为all 
		if(client.equals("null")){
			client="all";
		}
		String[] msg = { "启动该轮测试成功!", "启动该轮测试失败!" };
		String st=run.addTaskQueue(pid, client,rid );

		responseResult(st, msg);
	}

通过pid和rid在数据库中查询得到本轮测试要加入队列的脚本名称,而队列的名称我们用rid来命名,消息的频道通过cli来命名,并且该频道发布的信息就是rid。在客户端会订阅两个频道,客户端ip和all,如果它接收到这两个频道的信息,就会取得队列的名称rid,客户端监听方法如下,使用的python:

def get_pub_msg(self):
        r = redis.StrictRedis(host = "10.7.3.17", port = 6379)   #redis服务器地址
        p = r.pubsub()
        cli=self.get_cli_ip()     #获取本机ip
        p.subscribe(cli,"all")     # 订阅本机ip和all两个频道
        print("Start listening channal:"+cli+",all")
        for item in p.listen():            #一直监听这两个频道,直到获取到消息
            if item['type']=='message':
                return item

取到了队列名称就可以向队列取脚本名称了:

def get_task_case_name(self,queue_name,timeout=10):
        r = redis.StrictRedis(host = "10.7.3.17", port = 6379)
        t=r.blpop(queue_name, timeout)
        return t

代码只是展示一个思路,直接复制是跑不通的,因为里面很多方法是我自己封装的。大家可以根据这个思路用自己熟悉的语言去做。