自动化用例开发过程中的常见技巧:如何让用例支持多环境?

今天起我开了《自动化用例开发过程中的常见技巧》系列文章,来务实地讲下在开发自动化用例时会遇到的一些 开发技巧 ,我本来想把范围框定在 设计模式 上,但严格的讲有些 技巧 并不算设计模式,而且这样来可谈的内容也被限制了,所以就换成了 技巧 俩字。

本系列的文章不会干讲设计模式、高级语法特性,如果是这样子,你完全可以自己去看一些高级编程的书籍。我会每次抛出一个在开发用例中会出现的问题,看看如何结合这些开发技巧来解决。另外请注意: 解决一个问题的方法不会只有一个,不要让自己的思维被我限定住了

这系列文章适合以下读者:

  • 测试框架、平台开发者

  • 自动化测试用例编写者

本系列文章中主要使用Python来展示这类技巧,也可能会使用Golang,如果你对这两门语言不熟悉,不用担心,这些技巧也可以使用其他编程语言应用,没有编程语言的限制。

初识property装饰器

了解设计模式的人肯定知道『装饰模式』,不过这与Python下的装饰器并不完全是等价的,Python装饰器可以提供更加强大的能力。

这里先介绍下 property 装饰器,看下面这个简单的例子:

class People:

    def __init__(self, name: str):
        self._name = name
    
    @property
    def name(self) -> str:
        return self._name
    
    @name.setter
    def name(self, nn: str)
        self._name = nn

    def no_property(self):
        return self._name

我实例化该People,分别访问 nameno_property 两个属性,看下有什么区别:

>>> mike = Person("mike")
>>> mike.no_property
<bound method Person.no_property of >
>>> mike.name
'mike'

no_property 的打印结果应该是更加容易理解的:它是Person的一个方法,当你访问该方法, 不是调用时 ,自然是返回方法对象本身;而访问 name 的时候结果就有点让人意外,它的打印结果等同于 name() ,而非访问方法对象本身。

@property 装饰器是为了解决Python下没有真正意义的私有变量,导致对象的attribute会被外部调用、修改、删除等问题而产生的。在使用上,你可以理解为加上了 @property 装饰器后,访问该属性 obj.method 即为调用该方法 obj.method()

语法讲解点到即止,想更加深入的理解property,可以参考官方文档:https://docs.python.org/3/library/functions.html#property

自动化用例的多环境执行需求

一个常规的研发项目,一般都存在开发、测试、生产环境:

  • 开发环境用于开发用途,以及各模块联调,一般测试人员不介入

  • 测试环境用于测试目的,测试人员的主要工作环境

  • 生产环境,上线验证过程中测试人员会使用到

公司规模越大,对各类环境的定义会更加清晰、明确,环境种类也会进一步的细分,比如很多互联网公司都会有预发布环境、鄙司的『特性环境』等。

这样来就对自动化测试框架提出了一个需求: 自动化用例应当支持在不同环境里执行,并且对用例逻辑层透明无感。

前面一句话好懂,后面一句话我考虑了下,还是得稍作解释: 不能在用例里出现类似的代码

def test_login(self):
    if env = "dev":
        requests.post("http://biz.dev.company.net/login", data={"username":"admin", "password":"123456"})
     elif env = "test"
        requests.post("http://biz.test.company.net/login", data={"username":"admin", "password":"88888888"})
     else:
        requests.post("https://biz.company.com/login", data={"username":"superadmin", "password":"123sxcdas!@#!"})

曾经也有人告诉我怎么解决:这几个环境用同一个账号、密码呗

来认真分析下,要实现测试用例在多环境下执行,要解决哪些问题:

  • 不同环境的服务入口地址不同,一般还会有http/https的差别

  • 不同环境需要使用不同的测试数据

  • 一些中间件,比如数据库、消息队列、缓存服务的访问地址、账号、配置有差别

  • 不同环境的第三方回调地址有差别

  • 不同环境的配置需要整体切换,不要出现在开发环境里用了生产环境的数据的问题

以上都是些常见的问题,甚至有些业务功能在实现上都还存在差异,不过这种就不在我们讨论的范围内了。

如何解题

要在用例层对测试环境无感,需要把环境所用的数据抽象出来,如下图:

其实这是一个典型的桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。

拿上面出现的测试用例代码具象一点地讲:用例开发者需要抽象出 服务地址账号 两个对象,用例逻辑层只允许使用这些抽象的对象,而不能直接访问具体的数据,框架层面提供对这些抽象对象的具体实现。这样来上面的测试代码就可以改成:

def test_login(self):
    requests.post(entrypoint.biz+"/login", data={"username":data.account.username, "password":data.account.password})

我这边使用 property装饰器 来简易的实现桥接模式,框架层的代码示例如下:

from enum import Enum

class Environment(Enum):
    DEV = 0
    BETA = 1
    PROD= 2

env = Environment.BETA   # 作为全局的环境变量


class EntryPoint:
    _biz = {
        Environment.DEV: "http://biz.dev.company.net",
        Environment.BETA: "http://biz.beta.company.net",
        Environment.PROD: "https://biz.company.com"
    }
    
    @property
    def biz(self):
        return self._biz[env]

样例代码中我省略了测试数据部分,测试数据是个很大的话题,我之后会完整地去讲述

如果需要切换环境去执行,只要更新全局变量 env 就可以实现:

写在最后

这是本系列的第一篇文章,因为是写给测试人员看的,我不确定文章内容是否合适,有想法的童鞋可以回复告知我下感受,我好在今后的文章里调整。

另外,这系列的文章中的代码样例我会以jupyter notebook的形式push到github repo: https://github.com/jacexh/automation-testing-patterns ,感兴趣的可以点下star