背景

是的, 我这个 java 技术栈的测试人开始玩 python 了,公司产品向用户提供了 python 的 sdk,所以为了能针对 sdk 做一些东西。我这两天研究了一下 python 和 python 通用的测试框架。一开始在社区和群里问了一些小伙伴们使用的都是什么,发现大多数人用的都是 unittest+HTMLTestRunner。 可能是我被 java 界那庞大的测试生态圈惯坏了,第一天就已然受不了 unittest+ HTMLTestRunner 的这种组合。所谓工欲善其事必先利其器,我花了一点时间 google 了一下 python 界常用的一些测试框架,像 unittest2,nose,pyunit,doctest 什么的。 挨个去官网查看了一下使用方式。发现 pytest 还是比较符合我的期望的。而且我用 java 的时候就一直使用的 report 框架----allure 也支持了 pytest。 所以今天还是老规矩。入门科普,抛砖引玉,详尽的文档请参考:http://doc.pytest.org/en/latest/contents.html 以及: https://github.com/allure-framework/allure-pytest

执行方式

pytest 的执行方式非常简单, 它跟 xunit 系的框架不一样,测试类不需要继承任何 pytest 的类。 在运行 pytest 命令的时候,自然会有一套 case discover 的机制识别。只需要你的类是以也就是说你的测试类和方法可以跟普通方法是一样的。只需要你的测试文件的命名规则符合 test_*.py 或者 *_test.py。 例子如下:

class TestDataLoad():
    def test_data_load(self, me):
        source_table = None
        # 判断是否已经导入这个数据
        bank_data = [t for t in me.tables if t.name == 'test_gaofei01']
        if len(bank_data) != 0:
            source_table = bank_data.pop()
        else:
            source_table = me.new_local_table('./banking_2_23.csv', 'test_gaofei01', format_='csv',
                                              first_line_schema=False)

这个时候我们到目录下执行 py.test。你会发现已经自动发现了你的测试函数并执行。 不需要像其他框架一样继承某一个类或者在函数上使用某个装饰器。 关于用例运行的具体配置,可以参照链接:http://doc.pytest.org/en/latest/usage.html

Test Fixture

fixture 一词最早是在 xunit test patterns 看到的。我也不知道翻译过来到底该叫什么,可以认为是测试所依赖的所有东西,例如参数,测试数据,数据库连接等。 在其他测试框架中一般提供了@setup @teardown 这类的方法。python 中使用@pytest.fixture 这样一个装饰器。我们直接看一个最简单的例子。

class ProphetClientBase(object):
    @pytest.fixture(scope='session')
    @allure.step('登录')
    def me(self):
        client = ProphetClient('http://172.27.1.115:8888')
        assert client.login('Admin', '1234567'), '登录失败'
        return client.me

上面我们用了 pytest 提供的装饰器来创建我们的 fixture。 这个方法也很简单,主要负责登录并返回用户对象。 这个用户对象是大部分 case 需要用的。 所以我们把它定义为一个 fixture。在其他框架中我们用 setup method 的方式来做的,这样做有个缺点就是 setup method 无法传递数据给测试方法作为参数。但是 pytest 中我们可以像下面一样做:

def test_data_load(self, me):
    source_table = None

    # 判断是否已经导入这个数据
    bank_data = [t for t in me.tables if t.name == 'test_gaofei01']

上面我们定义测试方法的时候直接在参数中使用 me 这个参数。pytest 在运行的时候会自动的帮我们找到 me 这个 fixture 运行并将返回值赋值到 me 中。不用我们做其他的处理。 同样我们在 fixture 的定义中可以看到我们使用了 scope='session', 意思是这个 fixture 是属于 session 级别的,在一个 session 中只会运行一次。是不是有点像 testng 的 beforeTest 系列了~

after 系列

刚才说了 before 系列,现在我们说说 after 系列,也即是 teardown。 一个 fixture 有时候是需要进行销毁操作的。 其他框架使用 after 或者 teardown 方法。 但是 pytest 可以直接在 fixture 中定义这个操作。只需要使用 python 的关键字 yield 代替 return。 并在 yield 之后编写销毁操作。 举个例子,还是上面的登录的 fixture,现在我们这么写:

@pytest.fixture(scope='session')
@allure.step('登录')
def me(self):
    client = ProphetClient('http://172.27.1.115:8888')
    assert client.login('Admin', '1234567'), '登录失败'
    yield client.me
    client.me.logout()

上面我们用 yield 代替 return 后执行了 logout 操作。 这样就好像是生成器一般在所有使用这个 fixture 的测试执行结束后。运行了 logout 操作。同样 pytest 也支持 with 。。 as 大法来配合 yield。如下:

@pytest.fixture
def passwd():
    with open("/etc/passwd") as f:
        yield f.readlines()

def test_has_lines(passwd):
    assert len(passwd) >= 1
自省功能

额,我是直接按字面直接翻译了。 其实这个功能就像是 java 的反射一样,可以在 fixture 中动态获取测试模块的属性来动态的执行。直接给个例子, 我们在一个文件中定义 fixture

@pytest.fixture(scope="module")
def smtp(request):
    server = getattr(request.module, "smtpserver", "smtp.gmail.com")
    smtp = smtplib.SMTP(server)
    yield smtp
    print ("finalizing %s (%s)" % (smtp, server))
    smtp.close()

然后再另一个文件中定义测试模块

smtpserver = "mail.python.org"  # will be read by smtp fixture

def test_showhelo(smtp):
    assert 0, smtp.helo()

在上面的例子中我们在 fixture 中使用了 request 这个 pytest 提供的默认参数,它其中有一个功能是可以提供测试方法的运行环境 (我理解这个功能很像 java 反射的功能之一)。在 fixture 中我们通过 getattr 直接反射出了测试模块中一个叫 smtpserver 的属性。并使用这个属性启动 server。

参数化 fixture

我们可以参数化 fixture 并且让测试函数根据不同的 fixture 运行多次测试 (很像是数据驱动的一种实现)。例子如下:

@pytest.fixture(scope="module",
params=["smtp.gmail.com", "mail.python.org"])
def smtp(request):
    smtp = smtplib.SMTP(request.param)
    yield smtp
    print ("finalizing %s" % smtp)
    smtp.close()

很简单,直接使用 params 传入一个参数列表。然后再 fixture 中使用 request.param 使用当前的参数。 这样你会发现测试方法会使用这两个 fixture 执行两次。

使用方式

除了在测试方法中显示的使用 fixture 函数作为参数外,我们还可以让 fixture 传递参数而自动执行。只需要使用@pytest.mark.usefixtures("cleandir") 这个方式就可以了。

@user12res("cleandir")
class TestDirectoryInit:
    def test_cwd_starts_empty(self):
        assert os.listdir(os.getcwd()) == []
        with open("myfile", "w") as f:
            f.write("hello")
    def test_cwd_again_starts_empty(self):
        assert os.listdir(os.getcwd()) == []

上面的例子里在 class 的定义中使用这个装饰器。这样在运行这个 class 的测试方法前都会执行一次 fixture 方法。 好了关于 fixture 的主要就介绍这么多了。 详细的请看链接:http://doc.pytest.org/en/latest/fixture.html

测试方法的参数化

刚才说了 fixture 的使用方式,但是还是无法满足我们的日常需要,我们希望有更灵活的方式给测试方法进行参数化。例如我们日常的数据驱动的测试策略。 pytest 中同样给我们提供了这样的一个方式。看下面的例子:

import pytest
def value(xmlPath):
   # 读取参数文件的代码
  pa = [
        ("3+5", 8),
        ("2+4", 6),
        ("6*9", 54),
    ]
    return pa
@user13ize("test_input,expected", value(path))
def test_eval(test_input, expected):
    print(test_input)
    assert eval(test_input) == expected

上面是我为了实现我在 testng 中做数据驱动的功能而写的 demo。通过 parametrize 这个装饰器我们就实现了跟 testng 中的 data provider 很相似的功能。tesng 是接受一个 2 维数组,而 pytest 则是一个装有多个元祖的列表。这个我觉得可能不用多说了,知道数据驱动的同学都懂。

好了关于 pytest 我就先介绍到这吧,pytest 是个很大的框架但是使用起来很简单。我今天只介绍了我们常用的比较重要的功能。 详细的文档请参阅:http://doc.pytest.org/en/latest/contents.html

allure-pytest

下面来看看为了显示高大上的 report,我们抛弃了 HTMLTestRunner 而直接使用 allure-pytest。 我之前发过帖子专门说明了 allure 在 java 和 jenkins 上如何配置插件,详情请看:https://testerhome.com/topics/5738。 来我先安利一下效果图:

内置日志系统,展示清楚,分类方便,可以上传附件。 总之我太喜欢这个 report 框架了。使用方式也很简单,安装 pytest-allure-adaptor 这个模块后,执行测试的时候增加一个参数就好了。如下:

py.test --alluredir report

这样我们的测试报告就生成在 report 目录下了。跟 java 的标注不太一样,pytest 使用装饰器的样子大概是这样的。

step 这个装饰器就是我们页面上看到的日志功能,每调用一次这个方法,页面上就会显示这个日志。具体用法看我的帖子和 allure 的官方文档吧。

总结

好了,今天先说这么多,我初入 python 圈以后还忘大家多多指教


↙↙↙阅读原文可查看相关链接,并与作者交流