python测试框架

| 标签 python  pytest  浏览次数: -

Pytest官方文档地址

安装

  • pip安装

pip install pytest

编写测试用例规则

  • 测试文件以test_开头(以_test结尾也可以)
  • 测试以Test开头,并且不能带有 __init__ 方法
  • 测试函数test_开头
  • 断言使用基本的assert即可

测试函数

  • setup_module/teardown_module

在所有测试用例执行之前和之后执行。

  • setup_function/teardown_function

在每个测试用例之前和之后执行。

测试类

  • setup_class/teardown_class

在当前测试类的开始与结束执行。

  • setup/treadown

在每个测试方法开始与结束执行。

  • setup_method/teardown_method

在每个测试方法开始与结束执行,与setup/treadown级别相同。

测试函数执行顺序

按照函数声明顺序,依次执行

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def test_22():
    print("22222 22")
    assert True


def test_11():
    print("11111 11")
    assert True


def test_two():
    print("222 two")
    assert True


def test_one():
    print("1111 one")
    assert True

在命令行调用pytest

pytest -q test_restore_prepare_media.py pytest -v -s test_restore_prepare_media.py pytest test_restore_prepare_media.py

在python代码中调用pytest

test/
├── __init__.py
├── test_restore_prepare_media.py
└── test_main.py
import pytest

def test_main():
    assert 5 != 5

if __name__ == '__main__':
    pytest.main()
    pytest.main("-q test_main.py")   # 指定测试文件
    pytest.main("~/pyse/pytest/")  # 指定测试目录

断言

断言异常抛出

import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0
# 1/0的时候应该抛出ZeroDivisionError,否则用例失败,断言不通过

访问异常的具体信息

def test_recursion_depth():
    with pytest.raises(RuntimeError) as excinfo:
        def f():
            f()
        f()
    assert 'maximum recursion' in str(excinfo.value)

定制断言异常的错误信息

with raises(ZeroDivisionError, message="Expecting ZeroDivisionError"):
    pass
Failed: Expecting ZeroDivisionError

pytest.fixture

通过@pytest.fixture() 注释会在执行测试用例之前初始化操作.然后直接在测试用例的方法中就可以拿到初始化返回的参数(参数名要和初始化的方法名一样)

为可靠的和可重复执行的测试提供固定的基线。(可以理解为测试的固定配置,使不同范围的测试都能够获得统一的配置。)

fixture提供了区别于传统单元测试(setup/teardown)风格的令人惊喜的功能:

  • 1.有独立的命名,可以按照测试的用途来激活,比如用于functions/modules/classes甚至整个project。
  • 2.按模块单元的方式实现,每个fixture name可以出发一个fixture function,每个fixture function本身也能调用其他的fixture function。(相互调用,不只是用于test_func())。
  • 3.fixture的范围覆盖简单的单元测试到复杂的功能测试,可用于参数传入或者class、module及test session范围内的复用。

参数含义

  • scope:
    • function: 每次调用都会重新生成
    • class: 在类中只生成一次
    • module: 在这个模块中都不会重新生成
    • session: 在运行pytest生成一次
  • params: 参数值
  • autouse:
    • True: 每个测试函数调用时会自动调用
    • False: 有程序显示调用,不会自动触发

具体测试用例

场景: 我们需要判断用户的密码中包含简单密码,规则是这样的,密码必须至少6位,满足6位的话判断用户的密码不是password123或者password之类的弱密码。

  • 用户的信息文件users.dev.json
[
  {"name":"jack","password":"Iloverose"},
  {"name":"rose","password":"Ilovejack"},
  {"name":"tom","password":"password123"}
]
  • 测试文件test_user_password.py
import pytest
import json

class TestUserPassword(object):
    @pytest.fixture
    def users(self):
        return json.loads(open('./users.dev.json', 'r').read()) # 读取当前路径下的users.dev.json文件,返回的结果是dict

    def test_user_password(self, users):
        # 遍历每条user数据
        for user in users:
            passwd = user['password']
            assert len(passwd) >= 6
            msg = "user %s has a weak password" %(user['name'])
            assert passwd != 'password', msg
            assert passwd != 'password123', msg
  • 使用@pytest.fixture装饰器可以定义feature
  • 在用例的参数中传递fixture的名称以便直接调用fixture,拿到fixture的返回值
  • 3个assert是递进关系,前1个assert断言失败后,后面的assert是不会运行的,因此重要的assert放到前面
  • E AssertionError: user tom has a weak password可以很容易的判断出是哪条数据出了问题,所以定制可读性好的错误信息是很必要的
  • 任何1个断言失败以后,for循环就会退出,所以上面的用例1次只能发现1条错误数据,换句话说任何1个assert失败后,用例就终止运行了
  • 查看用例文件中可用的fixtures

pytest –fixtures test_user_password.py

  • 数据清洗

有时候我们需要在用例结束的时候去清理一些测试数据,或清除测试过程中创建的对象,我们可以使用下面的方式

import smtplib
import pytest

@pytest.fixture(scope="module")
def smtp1():
    smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    yield smtp  # provide the fixture value
    print("teardown smtp")
    smtp.close()


@pytest.fixture(scope="module")
def smtp2(request):
    smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    def fin():
        print ("teardown smtp")
        smtp.close()
    request.addfinalizer(fin)
    return smtp  # provide the fixture value
  • yield 关键字返回了fixture中实例化的对象smtp
  • module中的用例执行完成后smtp.close()方法会执行,无论用例的运行状态是怎么样的,都会执行

参数化fixture

允许我们向fixture提供参数,参数可以是list,该list中有几条数据,fixture就会运行几次,相应的测试用例也会运行几次。

其中len(params)的值就是用例执行的次数

@pytest.fixture(scope="module",
                params=["smtp.gmail.com", "mail.python.org"])
def smtp(request):
    smtp = smtplib.SMTP(request.param, 587, timeout=5)
    yield smtp
    print ("finalizing %s" % smtp)
    smtp.close()
    # 第1次request.param == 'smtp.gmail.com'
    # 第2次request.param == 'mail.python.org'

import pytest
import json
users = json.loads(open('./users.test.json', 'r').read())

class TestUserPasswordWithParam(object):
    @pytest.fixture(params=users)
    def user(self, request):
        return request.param

    def test_user_password(self, user):
        passwd = user['password']
        assert len(passwd) >= 6
        msg = "user %s has a weak password" %(user['name'])
        assert passwd != 'password', msg
        assert passwd != 'password123', msg

pytest.mark.parametrize

可以让我们每次参数化fixture的时候传入多个参数。因此简单理解,我们可以把parametrize装饰器想象成是数据表格,有表头(test_input,expected)以及具体的数据。

import pytest
@pytest.mark.parametrize("test_input,expected", [
    ("3+5", 8),
    ("2+4", 6),
    ("6*9", 42),
])
def test_eval(test_input, expected):
    assert eval(test_input) == expected

在classes,modules或者projects中使用fixtures

有时,测试函数是不直接访问一个fixture对象的。比如,测试需要用一个空的路径当作当前工作路径,但是并不关心当前的具体路径。下面的例子是用标准的tempfile库和pytest fixtures来实现的。我们将创建fixture的部分单独放到conftest.py中。

class

  • conftest.py
import pytest
import tempfile
import os

@pytest.fixture()
def cleandir():
    newpath = tempfile.mkdtemp()
    os.chdir(newpath)

  • test_setenv.py
import os
import pytest

@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit(object):
    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()) == []
  • 指定多个fixtures

@pytest.mark.usefixtures(“cleandir”, “anotherfixture”)

modules

  • 在test module的层级指定fixture的用途,通过使用标记机制的通用功能:

pytestmark = pytest.mark.usefixtures(“cleandir”)

被指定的变量必须命名为pytestmark,比如像foomark这样的是不能激活fixtures的。

project

  • pytest.ini
[pytest]
usefixtures = cleandir

Auto use fixtures (xUnit setup on steroids)

偶尔地,我们可能希望在不明确声明一个函数参数或一个usefixtures装饰器的情况下,让fixtures被调用。以一个实际情况为例,假设我们有一个database fixture有begin/rollback/commit的结构,我们想要让每个测试方法都自动地跟随一个事务和回滚。下面是这个概念的一个虚拟的独立实现:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pytest


class DB(object):
    def __init__(self):
        self.intransaction = []

    def begin(self, name):
        self.intransaction.append(name)

    def rollback(self):
        self.intransaction.pop()


@pytest.fixture(scope="module")
def db():
    return DB()


class TestClass(object):
    @pytest.fixture(autouse=True)
    def transact(self, request, db):
        db.begin(request.function.__name__)
        yield
        db.rollback()

    def test_method1(self, db):
        assert db.intransaction == ["test_method1"]

    def test_method2(self, db):
        assert db.intransaction == ["test_method2"]

在class层级transactfixture被autouse=True标记,这个标记是为了实现,让这个class里面的所有测试方法,不需要在测试函数标记或class层级使用usefixtures装饰器的前提下就能使用这个fixture。

下面是autouse fixtures怎么在其他scope下工作的:

  • autouse fixtures遵从scope=关键字参数:如果一个autouse fixture有scope="session",不管它在哪里定义都只会运行一次。scope='class'表示将会在每个class运行一次等等。
  • 如果一个autouse fixture在test module中定义,这个module中所有的测试函数将会自动使用它。
  • 如果一个autouse fixture定义在conftest.py中,该路径下的所有测试module下的所有测试函数都会调用这个fixture。
  • 最后,请小心的使用:如果你在插件中定义了一个autouse fixture,它将会在被安装的所有project的所有测试中调用。如果这个fixture无论如何都会在当前确定的settings下运行,比如在ini-file中,这样的设定非常有用。像这样一个全局的fixture应该快速确定它是否需要做任何工作,并避免不必要的imports或计算。

fixture functions查找顺序

如果你在实现测试的过程中发现一个fixture会用于多个测试,则可以将其移动到conftest.py中,或者甚至在不改变代码的情况下单独安装插件。fixture functions的查找顺序是test classestest modulesconftest.py文件,最后是builtin三方插件

常用技巧

基础用法

参数化

接口测试

需求分析

根据[3A](http://www.testclass.net/interface/3a/)原则,我们可以设计如下的用例
测试数据: 节点的名称:python/java/go/nodejs
接口地址: https://www.v2ex.com/api/nodes/show.json
断言: 返回的结果里,name字段的值必须等于传入的节点名称
  • 实现代码
import requests
import pytest

class TestV2exApiWithParams(object):
    domain = 'https://www.v2ex.com/'

    @pytest.fixture(params=['python', 'java', 'go', 'nodejs'])
    def lang(self, request):
        return request.param

    def test_node(self, lang):
        path = 'api/nodes/show.json?name=%s' %(lang)
        url = self.domain + path
        res = requests.get(url).json()
        assert res['name'] == lang
        assert 0

class TestV2exApiWithExpectation(object):
    domain = 'https://www.v2ex.com/'

    @pytest.mark.parametrize('name,node_id', [('python', 90), ('java', 63), ('go', 375), ('nodejs', 436)])

    def test_node(self, name, node_id):
        path = 'api/nodes/show.json?name=%s' %(name)
        url = self.domain + path
        res = requests.get(url).json()
        assert res['name'] == name
        assert res['id'] == node_id
        assert 0

生成xml格式的测试报告

pytest test_quick_start.py --junit-xml=report.xml

Selenium

  • 1.根据chrome浏览器版本下载对应版本的chromedirver驱动,版本对应
  • 2.解压chromedriver后移动到 $PATH 中(比如: /usr/local/bin 下)
sudo mv ~/Downloads/chromedriver /usr/local/bin

avocado

Unittest

Doctest

Nose

tox


上一篇 Python单例模式     下一篇 snakefood分析python依赖
目录导航