自动化用例开发过程中的常见技巧:连接复用

为什么需要连接复用

接口、UI的测试用例中都会有大量的IO操作,比如HTTP、RPC调用、数据库查询等,这是典型的IO密集型任务,对自动化效率有追求的测试工程师应该思考一个问题: 如何让用例执行更加地有效率(快)?

抛出的这个问题其实很大,从验证策略、用例设计、IO优化、用例分发方式等角度都可以讲,我不准备在这篇文章里完整的阐述,只挑出一个点: 连接复用

这里的 连接 可以存在于以下地方:

  • HTTP连接

  • RPC连接(http、socket都可能)

  • 中间件连接(数据库、缓存服务等连接,可简化为TCP)

  • UI自动化的Appium、Selenium对象(webdriver协议)

连接复用(以TCP为例)的好处可以大幅度降低TCP三次握手、四次挥手的次数以实现对用例消耗时间的降低,举一个很简单的例子:比如一个mysql client的建链跟关闭连接各需要10ms,当你存在10000多条用例,并且平均每个用例需要2次mysql查询操作,那整个用例执行时间可以降低400秒。对于做惯了UI自动化测试的童鞋而言,UI自动化执行时间往往以分钟、小时为计量单位,这400秒时间的减少似乎并不明显。这点我承认,但是对于下沉至接口层的自动化,完全可以相信一个业务场景用例能在一秒内验证完成,能压榨出400秒时间就是非常大的优化。

而且我相信,你在每一点上都比别人多想一点多做一点,这些点点滴滴的积累、沉淀就会变成你的绝对优势。

不经意来了碗鸡汤,回到正题:连接复用。

一般操作

对于测试人员而言,要实现『连接复用』最简单的办法对高度抽象的应用对象的复用,你不用过多去考虑实现层面的细节,比如连接池等。比如我之前在 接口封装的基石:requests.Session 介绍过通过 requests.Session 来实现HTTP连接的复用,当你所有的HTTP接口调用都基于同一个 requests.Session 来调用的话,那其实就实现了全局的『HTTP连接复用』能力。

HTTP调用是有状态的,所以是否应该使用同一个requests.Session来调用,要视实际情况来判断,本文不多展开。

下文我以mysql的连接复用(使用 pymysql 库)来作介绍。

先看一个简单的例子:

import pymysql

conn = pymysql.Connect(host="your_host", user="root", password="your_password", database="your_db")
with conn.cursor() as curosr:
    curosr.execute("select * from user limit 1")
    ret = curosr.fetchone()
conn.close()

当你在测试用例里需要进行SQL查询时,可以copy上面的代码去做相关的操作,一个两个用例还好,但是用例成千上百时,我就算不讲『连接复用』概念,我也相信你也觉得这样的代码很臃肿,需要优化。

大部分测试人员会使用这个办法:在测试启动时,连接一次数据库( pymysql.Connect ),然后把返回的 pymysql.Connection 作为一个全局对象供其他用例使用,这就是 连接复用 的思路。

现实问题

但往往我们实际的应用场景可能更加丰富、复杂,比如:

  1. 需要访问同一数据库实例的不同database

  2. 需要不同账号访问同一数据库实例(权限问题)

  3. 需要访问不同数据库实例

第一种情况还好,访问不同database可以共用一个连接,只需要使用 use <db> 来切换。另外两种呢?如果按照上面提到的思路也有办法:在测试启动时,建立不同账号建立对不同数据库实例的连接,都是作为『全局的数据连接』,而在使用时(用例逻辑层)去挑选适合你当前用例的连接对象。

按照上面办法的需要注意:因为需要用例设计者人工去选择合适的 pymysql.Connection 对象,当对象较多时,用例设计者很可能选错,导致用例失败。

我这里更推荐另外种做法——懒加载,你不需要测试一开始就建立所有的mysql连接,而是在你的用例里需要去查询数据库时,显式地传入连接信息(地址、用户名等)去建立连接,这样就可以避免使用了错误的数据库连接信息了,如:

def test_user():
    conn = pymysql.Connect(host="host1", user="root", password="pwd", database="ddd")
    with conn.cursor() as curosr:
        curosr.execute("select * from user limit 1")
        ret = curosr.fetchone()
    assert ret


def test_tag():
    conn = pymysql.Connect(host="host1", user="root", password="pwd", database="ddd")
    with conn.cursor() as curosr:
        curosr.execute("select * from tag")
        ret = curosr.fetchone()
    assert ret

但这样就带出来问题了: 明明要讲连接复用,为什么还要在每一个用例里去初始化数据库连接?

单例模式

上面一大段其实就为了引出设计模式里非常重要的一种——单例模式: 单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

也就是说会存在以下的逻辑:

单例模式的实现办法有很多种,比如:

def singleton(cls):
    instances = dict()

    @functools.wraps(cls)
    def _singleton(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return _singleton


@singleton
class MySQLConnectionProxy:

    def __init__(self, *args, **kwargs):
        self._conn = pymysql.Connect(*args, **kwargs)

    def __getattr__(self, item):
        return getattr(self._conn, item)

上面的例子还用到了代理模式,之后会有更详细的讲解

对应的测试用例可以改成这种方式:

def test_user():
    conn = MySQLConnectionProxy(host="host1", user="root", password="pwd", database="ddd")
    with conn.cursor() as curosr:
        curosr.execute("select * from user limit 1")
        ret = curosr.fetchone()
    assert ret

再结合我们上一讲的 如何让用例支持多环境? ,我们可以把数据库连接信息抽象出来,从而变成:

def test_user():
    conn = MySQLConnectionProxy(**entrypoints.mysql)
    with conn.cursor() as curosr:
        curosr.execute("select * from user limit 1")
        ret = curosr.fetchone()
    assert ret

单例模式的变种

但上面单例模式的代码其实并没有解决多用户、多数据库连接的问题,该怎么解决呢?思路稍微变通下不难发现: 应该只对使用相同连接信息的调用使用单例模式

这话说点有点抽象,具象一点就是:当数据库host、端口、用户名、密码相同时,返回一个已建立的 pymysql.Connection ,也可以用下图来加深理解:

所以可以进一步优化上面的代码:

def singleton_mysql_instance(cls):
    instances = dict()

    @functools.wraps(cls)
    def _singleton(*args, **kwargs):
        conn_params = (kwargs.get("host"), kwargs.get("port"), kwargs.get("user"), kwargs.get("password"))
        p = hash(conn_params)
        if p not in instances:
            instances[p] = cls(*args, **kwargs)
        return instances[p]
    return _singleton

为了方便理解,我简化了实现,也尽量少去使用inspect、magic method这些能力

连接复用的注意点

单例模式下全局只维护了一个实例,这个时候一定要慎重考虑一个问题:如果该对象被执行了析构函数或者像mysql的连接被关闭了(不管是主动还是被动),如何能够发现或者重新构造?

另外还有一个问题,全局只维护了一个实例,在多线程模型下,是否能够保证对它的操作是线程安全的?(thread safety)

受限于篇幅,这两个问题这边不展开讨论了,感兴趣的可以留言一起讨论。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章