自动化用例开发过程中的常见技巧:代理模式

在上一次讲连接复用的时候,我实现了一个类用于接管 pymysql.Connection

class MySQLConnectionProxy:

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

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

这其实是一个很典型的 Proxy Pattern :给某一个对象提供一个代理,并由代理对象控制对原对象的引用

具体完整解释请参考维基百科词条:https://en.wikipedia.org/wiki/Proxy_pattern

代理模式应该是一种比较容易理解的设计模式,你可以把它类比成服务部署中的 nginxapache http 这类服务,它不暴露原始的请求资源地址(对象),而是让nginx(proxy)来接管client(调用方)的所有请求,具备了通过nginx(proxy)植入一些额外的能力来实现对原始资源的扩展、控制等。

这种模式的调用时序可以看下图:

我个人认为代理模式存在几点优势:

  • 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。

  • 代理模式可以控制对真实对象的使用权限

  • 代理接管了对真实对象的调用,实现AOP(切面编程)的能力

实现一个Proxy类

在Python中因为有着 __getattr____setattr__ 这样的magic method,所以实现一个通用的Proxy类是非常方便的事情:

class Proxy:
    def __init__(self, subject):
        self._subject = subject
    
    def __getattr__(self, item):
        return getattr(self._subject, item)
    
    def __setattr__(self, item, value):
        if item == "_subject":
            super().__setattr__(item, value)
        else:
            setattr(self._subject, item, value)

    
class Student:
    def __init__(self, name, age: int):
        self.name = name
        self.age = age
    
    def info(self):
        return "name: {}\tage:{}".format(self.name, self.age)
mike = Proxy(Student("mike", 20))

print(mike.info())
mike.age = 24
print(mike.info())
print(type(mike))

name: mike  age:20
name: mike  age:24

可以看到上面十行代码就实现了通用的Proxy,不过这样的Proxy看上去并没有太多实际作用,于是我稍微扩展下,让其能够实现 beforeafter 这样的事件钩子:

class Proxy:
    def __init__(self, subject):
        self._subject = subject
        self._handlers = dict()
    
    def __getattr__(self, item):
        before = "before_{}".format(item)
        if before in self._handlers:
            self._handlers[before](item)
        
        ret = getattr(self._subject, item)
        
        after = "after_{}".format(item)
        if after in self._handlers:
            self._handlers[after](item)
        return ret
    
    def register(self, handler, method: str, scope: str):
        """注册任意函数的拦截器,实现对任意函数的after、before的钩子"""
        self._handlers["{}_{}".format(scope, method)] = handler
        
    
    def __setattr__(self, item, value):
        if item == "_subject" or item == "_handlers":
            super().__setattr__(item, value)
        else:
            setattr(self._subject, item, value)
            
  def print_before(item):
    print("before invoke {} method".format(item))

def print_after(item):
    print("after invoke {} method".format(item))


jack = Proxy(Student("jack", 16))
jack.register(print_before, "info", "before")
jack.register(print_after, "info", "after")
jack.info()
>>> before invoke info method
>>> after invoke info method
>>> 'name: jack\tage:16'

以上代码可能会对不太熟悉Python编程的自动化测试人员产生很大的困扰,不用太紧张,如果在你的项目要应用到代理模式,你可以针对要具体代理的对象进行具体的实现,不需要用到这么多magic method造成理解的障碍。

自动化测试用的应用

在上一次讲连接复用的末尾,我抛出了几个问题:

  • 如果mysql连接被服务端主动关闭了怎么办?

  • 因为是单例模式,如何防止有用例主动关闭mysql连接而影响其他用例?

然后我再抛出一个需求: 如何让框架自动记录mysql的查询记录,而不是手动去打日志?

以上三个问题其实在代理模式下都可以非常方便、优雅的来解决掉:

import pymysql


class CursorProxy(Proxy):
    
    def execute(*args, **kwargs):
        # 这里可以实现日志记录
        return getattr(self._subject, "execute")(*args, **kwargs)
        


class MySQLConnectionProxy:

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

    def __getattr__(self, item):
        if self._conn.open:  # 这里可以检查是否被动关闭,然后实现重连
            self._conn.connect()
        
        if item == "close":  # 当用例要主动关闭时,无视该调用即可
            return
        
        if item == "cursor":  # 游标对象也需要代理
            def _curor(*args, **kwargs):
                return CurorProxy(getattr(self._subject, item)(*args, **kwargs))
            return _curor
        
        return getattr(self._conn, item)

结合这两节的内容来看,通过不太多的代码行数,在目前的自动化测试框架中已经实现了中间件client以下能力:

  • 连接的复用

  • 连接的自动重试、关闭保护

  • client请求内容的自动化日志记录

最后我抛出一个注意事项:在Python下使用代理模式后,能不能保留下原对象的上下文管理器(Context Manager)的特性?感兴趣的童鞋可以留言告知