1. Pytest 基础

1.1 初始化/清除

初始化/清除功能对于自动化测试框架而言非常重要,假设现在先测试了一个注册功能,如果测试完该功能没有进行清除操作,则测试产生的数据可能会影响到后面的功能测试。

1.1.1 模块级别

作用:为模块中所有的测试用例做公共的初始化和清除

模块级别的初始化(setup_module)和清除(teardown_module),只会在模块的测试用例执行之前和执行之后各执行 1 次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def setup_module():
print('\n *** 初始化-模块 ***')


def teardown_module():
print('\n *** 清除-模块 ***')

class Test_错误密码:

def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2


class Test_错误密码2:

def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

执行上面代码后,可以通过终端打印的结果看到 setup_module() 和 teardown_module() 所执行的顺序是不同的,一个是在用例执行前进行初始化操作,一个是在用例执行后进行清除操作。

1.1.2 类级别

作用:为类中所有的测试用例做公共的初始化和清除,函数必须放在类中才起作用

类级别的初始化(setup_class)和清除(teardown_class),只会在测试类中的测试用例执行之前和执行之后各执行 1 次,可以选择指定的类执行初始化和清除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def setup_module():
print('\n *** 初始化-模块 ***')

def teardown_module():
print('\n *** 清除-模块 ***')

class Test_错误密码:

@classmethod
def setup_class(cls):
print('\n === 初始化-类 ===')

@classmethod
def teardown_class(cls):
print('\n === 清除-类 ===')

def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

class Test_错误密码2:

def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

通过打印结果也可以看到,上面代码中的类级别只是在指定的 Test_错误密码 类的用例执行前后进行了初始化和清除操作,并没有关系到 Test_错误密码2 类。

1.1.3 方法级别

作用:为方法中所有的测试用例做公共的初始化和清除,函数必须放在类中才起作用

方法级别的初始化(setup_method)和清除(teardown_method),会在方法的测试用例执行之前和执行之后各执行 1 次,可以选择指定的类执行初始化和清除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def setup_module():
print('\n *** 初始化-模块 ***')

def teardown_module():
print('\n *** 清除-模块 ***')

class Test_错误密码:

@classmethod
def setup_class(cls):
print('\n === 初始化-类 ===')

@classmethod
def teardown_class(cls):
print('\n === 清除-类 ===')

def setup_method(self):
print('\n --- 初始化-方法 ---')

def teardown_method(self):
print('\n --- 清除-方法 ---')

def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

class Test_错误密码2:

def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

通过打印结果可以发现,方法级别的初始化和清除会在类中每个用例执行的前后都会操作一次。

1.2 挑选用例执行

1.2.1 指定模块

1
pytest cases\登录\test_错误登录.py

1.2.2 指定目录

  • 指定单个目录执行
1
pytest cases
  • 指定多个目录执行
1
pytest cases1 cases2\登录

1.2.3 指定模块里的函数或类

  • 指定单个类
1
python -m pytest cases\登录\test_错误登录.py::Test_错误密码
  • 指定类中的函数
1
python -m pytest cases\登录\test_错误登录.py::Test_错误密码::test_C001001

1.2.4 根据输入字符

在命令添加参数 -k,然后可以继续添加一串字符来挑选要执行的测试用例,例如我在后面添加一个函数的名称:

1
python -m pytest -k C001001 -s

-k 后面可添加的字符规则如下:

  • 可以是函数/类/模块文件/目录的名称
  • 是大小写敏感的
  • 不一定要完整,只要能有部分匹配上即可
  • not 关键字:不包含后续字符
1
python -m pytest -k "not C001001" -s
  • and 关键字:筛选时需同时包含多个字符
1
python -m pytest -k "错 and 密码2" -s
  • or 关键字:筛选时只需包含其中一个字符即可
1
python -m pytest -k "错 or 密码2" -s

1.2.5 根据标签

标签定义时,根据功能可以大致分为三类:

  • 环境标签
  • 用例作者标签(这个用例是谁负责或谁写的)
  • 特定用途标签(例如 smoke 冒烟测试标签)

注意:

标签支持中英文,如果想一次指定多个标签运行的话,not、and 和 or 关键字仍能使用在这,例如 python -m pytest cases -m "标签1 or 标签2"。

  • 给某个方法打上标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pytest

class Test_错误密码1_1:
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

class Test_错误密码1_2:
@pytest.mark.webtest
def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

运行时指定该标签,则只会运行该标签下的用例。

1
python -m pytest cases -m webtest -s

  • 给某个类打上标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pytest

class Test_错误密码1_1:
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

@pytest.mark.webtest
class Test_错误密码1_2:
def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

  • 添加多个标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pytest

@pytest.mark.登录测试
class Test_错误密码1_1:
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

@pytest.mark.登录测试
@pytest.mark.webtest
class Test_错误密码1_2:
def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

webtest 标签运行结果

登录测试标签运行结果

  • 设置全局变量,为整个模块文件设定标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest
pytestmark = pytest.mark.网页测试
class Test_错误密码1_1:
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

class Test_错误密码1_2:
def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

  • 全局变量也可以定义多个标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest
pytestmark = [pytest.mark.网页测试, pytest.mark.登录测试]
class Test_错误密码1_1:
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

class Test_错误密码1_2:
def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

6.在一个文件夹中统一管理标签

例如我在 mark.py 文件中管理全部标签。

1
2
3
4
import pytest

login_test = pytest.mark.登录测试
web_test = @pytest.mark.webtest

然后在主文件中进行导入使用,这样可以减少后期维护成本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from mark import *

class Test_错误密码1_1:
@login_test
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

@web_test
class Test_错误密码1_2:
def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

解决自定义标签运行时发出警告问题

我们可以通过项目的根目录下的配置文件 pytest.ini,并在里面设置标签配置项,其中冒号后面是标签的说明。

1
2
3
4
5
[pytest]
markers =
webtest: web tests
网页测试: 标记为网页测试

1.3 跳过用例执行

在自动化测试过程中,经常会遇到功能阻塞、功能未实现、环境等一系列外部因素问题导致用例无法正常执行。如果我们注释掉或删除掉,后面还要进行恢复操作就会比较麻烦,而这时我们就可以通过 skip 标签来跳过用例。

  • skip 跳过,无条件和原因:@pytest.mark.skipif()
  • skip 跳过,无需满足条件 true、有跳过原因:@pytest.mark.skipif(reason='无条件,只有跳过原因')
  • skip 跳过,需满足条件 true、且有跳过原因:@pytest.mark.skipif(条件1==1, reason='跳过原因')
  • skip 赋值变量,多处调用:myskip=pytest.mark.skipif(1==1, reason='skip赋值给变量,可多处调用')

1.3.1 无条件跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest

class Test_错误密码:
@pytest.mark.skip(reason="暂时跳过")
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 3

1.3.2 有条件跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest

class Test_错误密码:
flag=1

@pytest.mark.skip(reason="暂时跳过")
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

@pytest.mark.skipif(flag==1, reason="暂时跳过")
def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

@pytest.mark.skipif(flag==0, reason="暂时跳过")
def test_C001003(self):
print('\n用例C001003')
assert 3 == 3

我们也可以将 skip 标签赋值到一个变量,然后通过变量代替各个地方的 skip 标签,下面代码效果和上面是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pytest

class Test_错误密码:
flag=1
my_skip=@pytest.mark.skipif(flag==1, reason="暂时跳过")

@pytest.mark.skip(reason="暂时跳过")
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

@my_skip
def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

@my_skip
def test_C001003(self):
print('\n用例C001003')
assert 3 == 3

2. 插件详解

2.1 用例执行顺序

在 unittest 中,用例的执行顺序是按照 ASCII 的大小来决定的。而在 pytest 中,则是默认从上到下执行用例。

如果想改变执行顺序的话,则需要使用 mark 标记。

安装

1
pip install pytest-ordering

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pytest
class Test_错误密码:
@pytest.mark.run(order=3)
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

@pytest.mark.run(order=2)
def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

@pytest.mark.run(order=1)
def test_C001003(self):
print('\n用例C001003')
assert 3 == 3

如果没有使用命令行运行,而是使用 Pycharm 编辑器运行功能执行代码,则可能会出现按照 ASCII 码的大小顺序排序的情况,这可能是受到了编辑器内部规则的影响。

2.2 用例重试

2.2.1 Pytest-rerunfailures

安装

1
pip install pytest-rerunfailures

语法

1
2
python -m pytest cases -sv --reruns 重试次数
python -m pytest cases -sv --reruns 重试次数 --reruns-delay 次数之间的演示间隔设置(单位:秒)

示例

在有些场景下,可能因为网络波动等其它因素报错,但这些功能很可能没有问题,这就会导致用例执行不通过。因此我们可以设置失败重试次数,来提高容错性。

1
python -m pytest cases -sv --reruns 2

如果想每次执行失败后不要立即重跑,则可以通过参数 --reruns-delay 指定每次重跑之间的间隔等待时间,注意这个间隔时间的单位为秒。

1
python -m pytest cases -sv --reruns 2 --reruns-delay 1

2.2.2 Flaky

pytest-rerunfailures 是针对全部用例进行重试,如果我想指定某些方法重试或者不重试则需要使用 flaky 插件。

安装

1
pip install flaky

语法

  • max_runs:最大运行次数,包括初始运行和重试运行。默认值为 2,并且要大于 min_passes,否则报错。
  • min_passes:表示需要指定次数来明确需要连续成功运行多少次才能被视为成功,默认值为 1。
  • rerun_filter:表示指定一个过滤函数,当函数返回值为布尔值 False 时,会让预期失败的测试立即失败,而不是在失败后重新运行;当函数布尔值为 True 时,失败用例会重新执行。
1
@flaky(max_runs=NUM1, min_passes=NUM2, rerun_filter=func)

示例

例如我现在只想对下面的第 3 条用例进行重试,其它用例不重试,则可以将 flaky 装饰器加到指定的用例上,这样就只会对该用例进行重试操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import random
import pytest
from flaky import flaky

def test_demo_01():
b = 1 + 2
print("执行用例1")
assert 3 == b

def test_demo_02():
b = 1 + 2
print("执行用例2")
assert 2 == b

@flaky(max_runs=3)
def test_demo_03():
print("执行用例3")
flag = random.choice([False, True])
print(flag)
assert flag

if __name__ == '__main__':
pytest.main()

我们再来看一下 rerun_filter 的用法,下面这个例子中,rerun_filter 被赋值的函数始终返回 False,因此该用例只会有两种结果:

  • random 抽取到 True 时,用例会直接通过。
  • random 抽取到 False 时,由于 flaky 中的 max_runs 大于 1,所以用例会尝试重试。但 rerun_filter 中的函数返回 False,因此重试会被中断即禁止了此次重试,从而该用例最终会直接执行失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import random
import pytest
from flaky import flaky

def func(err, *args):
return False

@flaky(max_runs=3, rerun_filter=func)
def test_demo_04():
print("执行用例4")
flag = random.choice([False, True])
print(flag)
assert flag

if __name__ == '__main__':
pytest.main()

执行通过

执行失败

另外,我们也可以通过 rerun_filter 参数达到 pytest-rerunfailures 重试间隔机制的效果,即在赋值的函数中,进行一段时间的睡眠,然后再返回 True。如下例所示,这个用例如果第一次没有通过,则会睡眠 1 秒后再次重试。

1
2
3
4
5
6
7
8
9
import time

def delay_rerun(*args):
time.sleep(1)
return True

@flaky(rerun_filter=delay_rerun)
def test_something_else():
...

2.2.3 Tenacity

安装

1
pip install tenacity

语法

  • wait:设置重试间隔时间
  • stop:设置重试终止条件
  • retry:设置重试条件
1
@retry(...)

它实现了几乎我们可以使用到的所有重试场景,比如:

  • 在什么情况下才进行重试?
  • 重试几次呢?
  • 重试多久后结束?
  • 每次重试的间隔多长呢?
  • 重试失败后的回调?

示例

  • 一直重试
1
2
3
4
5
6
7
8
9
10
11
import pytest
from tenacity import retry

# 失败会一直重试,不会停止
@retry
def test_retry1():
print("等待重试,重试无间隔执行。。。")
raise 1==2

if __name__ == '__main__':
pytest.main()

  • 设置重试间隔时间
1
2
3
4
5
6
7
8
9
10
11
import pytest
from tenacity import retry

# 每次重试之间,间隔 3 秒
@retry(wait=wait_fixed(3))
def test_retry2():
print("等待重试。。。")
assert False

if __name__ == '__main__':
pytest.main()
  • 设置重试终止条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest
from tenacity import retry, wait_fixed, stop_after_attempt, stop_after_delay

# 重试间隔时间为 3 秒,且最多只会运行 3 次就会终止重试
@retry(wait=wait_fixed(3), stop=stop_after_attempt(3))
def test_retry3():
print("等待重试。。。")
assert False

# 重试间隔时间为 3 秒,且最多只会重试 7 秒就会终止重试
@retry(wait=wait_fixed(3), stop=stop_after_delay(7))
def test_retry4():
print("等待重试。。。")
assert False

# 重试间隔时间为 3 秒,且重试 7 秒或者运行 3 次就会终止重试
@retry(wait=wait_fixed(3), stop=stop_after_delay(7) | stop_after_attempt(3))
def test_retry5():
print("等待重试。。。")
assert False

if __name__ == '__main__':
pytest.main()

  • 设置重试条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pytest
from tenacity import retry, retry_if_result, stop_after_attempt

# 如果 test_retry 返回的结果为 False,则继续重试
def is_false(result):
return result is False

@retry(retry=retry_if_result(is_false), stop=stop_after_attempt(3))
def test_retry6():
print("等待重试...")
return False

if __name__ == '__main__':
pytest.main()

2.3 多线程

为了在测试用例数量特别多的情况下能够提升运行的效率,则可以采用多线程或者分布式运行方式来运行用例。

安装

1
pip install pytest-xdist

示例

我们可以模拟一下这个场景,现在有 4 个方法都会先睡眠 3 秒钟再执行,如果按原先的方式执行用例,则会按照顺序一个一个执行方法,效率会比较低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
class Test_错误密码:

def test_C001001(self):
time.sleep(3)
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
time.sleep(3)
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
time.sleep(3)
print('\n用例C001003')
assert 3 == 2

def test_C001004(self):
time.sleep(3)
print('\n用例C001004')
assert 4 == 4

现在我们可以尝试一下调用 2 个线程来执行用例,看一下结果如何 。

1
python -m pytest cases -sv -n 2

可以发现,使用多线程运行后测试的效率明显提升,每个线程都分配到了一个方法来执行。

没有使用 -n

使用 -n 多线程执行

2.4 生成报告

安装

1
pip install pytest-html

示例

pytest-html 插件就是用于生成测试报告的,前面已经安装过了。现在只需在命令行后加上参数 --html=report.html 即可,另外为了不额外产生冗余的目录,我们也加上 --self-contained-html 参数。

1
python -m pytest cases --html=report.html --self-contained-html

此时根目录下就会生成一个 report.html 的文件,在浏览器打开它就能看到详细的测试报告。

2.5 异常断言示例

1
2
3
4
5
6
7
8
9
10
11
12
13
import pytest

def test_case01_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0

def test_case02_zero_division():
with pytest.raises(ZeroDivisionError) as excinfo:
1 / 0
assert 'division by zero' in str(excinfo.value)

if __name__ == '__main__':
pytest.main()

3. Fixture 详解

使用 fixture 前须知:

  • 定义 fixture 跟定义普通函数差不多,唯一区别就是在函数上加个装饰器 @pytest.fixture(),fixture 命名不要用 test 开头,需要跟用例区分开
  • fixture 装饰器里的 scope 有四个级别的参数:function(默认)、class、module、package/session
  • fixture 可以有返回值,如果没有 return,默认会是 None;用例调用 fixture 的返回值,就是直接把 fixture 的函数名称作为参数传入
  • fixture 可以返回一个元组、列表或字典
  • 测试用例可传单个、多个 fixture 参数
  • fixture 与 fixture 间可相互调用

3.1 作为参数传递

fixture 的名字可以直接作为测试用例的参数,用例调用 fixture 的返回值,直接将 fixture 的函数名称当做变量名称。

如果用例需要用到多个 fixture 的返回数据,fixture 也可以返回一个元组,list 或字典,然后从里面去除对应数据。

直接看一个简单的示例,下面通过 fixture 定义了两个函数,并将这两个函数通过参数的形式传递到了 test_login 用例中,而这个用例可以直接使用 fixture 函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest

@pytest.fixture()
def username():
print("===== 打印 username 装饰器函数 =====")
username = 'gdx'
assert username == 'gdx'
return username

@pytest.fixture()
def password():
print("===== 打印 psw 装饰器函数 =====")
psw = 123456
assert psw == 123456
return psw

def test_login(username, password):
print(username, password)
assert username == 'gdx'
assert password == 123456

if __name__ == '__main__':
pytest.main(['-s', 'parse.py'])

注意:fixture 函数作为参数传递到用例中时,在执行用例之前,会先执行 fixture 函数,才会执行用例。

另外,fixture 定义的函数的返回值也可以是各种类型,直接看下面这个例子就能清楚了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pytest

@pytest.fixture()
def user():
print("===== 打印装饰器函数 =====")
username = 'gdx'
psw = 123456
return username, psw

def test_login(user):
print(user)
assert user[0] == 'gdx'
assert user[1] == 123456

@pytest.fixture()
def func():
obj = {"a": 1, "b": 2, "c": 3}
return obj

def test_dict(func):
print("\nin test_dict")
for key, value in func.items():
print(key, value)

if __name__ == '__main__':
pytest.main(['-s', 'parse.py'])

3.2 前置/后置

我们在前面已经了解过如何用 setup 和 teardown 等相关函数进行初始化/清除相关操作,但是它们无法覆盖到目录级别下,并且使用起来灵活度还是没有那么高,而 fixture 这个装饰器则可以很好的解决这些问题。

3.2.1 利用参数传递

fixture 前置/后置操作能够覆盖到方法、类、模块以及目录级别,我们这里就先来看看 fixture 如何利用参数传递的方式达到 setup 和 teardown 的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pytest

@pytest.fixture()
def func():
print("run before testcase......")
yield
print("run after testcase......")

def test_01(func):
print("in test_01")

def test_02():
print("in test_02")

if __name__ == '__main__':
pytest.main(['-s', 'parse.py'])

上面使用了 yield 语句,在 python 中,yield 是一个关键字,用于定义生成器函数。生成器函数是一种特殊的函数,可以在迭代过程中按需生成值,而不是一次性生成所有值。

当我们在上面函数中使用 yield 语句时,执行完初始化操作后函数会暂停执行,并将控制权返回给测试用例。在测试用例执行完后,就会使用这个 fixture,它会从上次暂停的地方继续执行。

简单来说,yield 语句之前的代码会在测试用例之前执行,yield 之后的代码会在测试用例执行完成之后再执行。

通过参数传递这种方式实现,我们可以发现只要不传 fixture 函数到用例中,前置/后置操作就不会实现,这样我们就可以选择性的对用例进行前置/后置除操作。

当然,传递到用例中的 fixture 函数可以是多个,这样会先进行一系列初始化操作,然后执行用例代码,最后再进行一系列的清除操作。

而前置/后置的顺序类似于 C++ 中构造函数和析构函数的初始化与清除的顺序,我们再来看个例子帮助我们理解,重点关注下面 test_02 的输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pytest

@pytest.fixture
def db():
print("in fixture db before testcase......")
yield
print("in fixture db after testcase......")

@pytest.fixture()
def redis():
print("in fixture redis before testcase......")
yield
print("in fixture redis after testcase......")

def test_01(db):
print("in test_01")

def test_02(db, redis):
print("in test_02")

if __name__ == '__main__':
pytest.main(['-s', 'parse.py'])

通过 test_02 的执行结果就能发现,最先执行初始化操作的 fixture 函数,在清除操作的时候就会在最后面,这也类似一个栈的先进后出的原理。

趁热打铁,我们再来看一段更贴近于真实场景的代码。在下面的 fixture 函数中,yield 后面不再为空,而是多了一个返回值,在 yield 后面返回的内容,会被用例执行的参数接收。

也就是说下面 yield 后面返回了一个 driver 变量,用例 test_search 执行的时候,就可以通过 driver_fixture 这个入参接收到 yield 返回的 driver 变量来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@pytest.fixture
def driver_fixture():
"""实现浏览器的打开和关闭"""
driver = webdriver.Chrome() # 等同于setup
yield driver
driver.quit() # 等同于teardown

def test_search(driver_fixture):
"""访问百度首页,搜索 pytest 字符串是否在页面源码中"""
driver = driver_fixture
driver.get('http://www.baidu.com')
driver.find_element_by_id('kw').send_keys('pytest')
driver.find_element_by_id('su').click()
time.sleep(3)
source = driver.page_source
assert 'pytest' in source

3.2.2 利用 Usefixtures

如果一个方法或者一个 class 用例想要同时调用多个 fixture,除了通过我们上面这种传递多个参数的形式外,还可以使用 @pytest.mark.usefixture() 进行叠加。注意叠加顺序,先执行的放底层,后执行的放上层。

还是来看个例子帮助快速理解,这里需要注重叠加顺序,下面的 test_01 和 test_03 用例的 fixture 函数的执行顺序是一样,从最终结果就可以看出。

使用 usefixtures 这种方法需要注意,它是无法获取到 yield 后面的返回值的,如果想通过 yield 返回一些数值到用例中使用,还是建议使用参数传递的方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pytest

@pytest.fixture
def func1():
a = 123
print("in fixture func1 before testcase......")
yield a
print("in fixture func1 after testcase......")

@pytest.fixture
def func2():
print("in fixture func2 before testcase......")
yield
print("in fixture func2 after testcase......")

def test_01(func1, func2):
new_a = func1
print(new_a)
print("in test_01")

@pytest.mark.usefixtures("func1")
def test_02():
print("in test_02")

@pytest.mark.usefixtures("func2")
@pytest.mark.usefixtures("func1")
def test_03():
print("in test_03")

if __name__ == '__main__':
pytest.main(['-s', 'parse.py'])

3.2.3 利用 Scope

接着,我们再来看看 setup 和 teardown 无法覆盖的目录级别,在 fixture 下是如何实现的。

conftest.py 文件使用须知:

  • 可以跨 .py 文件调用,有多个 .py 文件调用时,可让 conftest.py 只调用了一次或调用多次 fixture
  • conftest.py 与运行的用例要在同一个 pakage 下
  • 不需要 import 导入 conftest.py,pytest 用例会自动识别该文件,放到项目的根目录下就可以全局目录调用了,如果放到某个 package 下,那就在 package 内有效
  • conftest.py 配置脚本名称是固定的,不能改名称
  • conftest.py 文件不能被其他文件导入
  • 所有同目录下测试文件运行前都会执行 conftest.py 文件

如果想对一个目录下的所有文件进行公共的初始化和清除操作,就可以在该目录下创建一个 conftest.py 的文件,在这里编写目录公共的初始化和清除代码。

1
2
3
4
5
6
7
import pytest 

@pytest.fixture(scope='package', autouse=True)
def st_emptyEnv():
print(f'\n#### 初始化-目录甲')
yield
print(f'\n#### 清除-目录甲')

当用例很多的时候,每次都将 fixture 作为参数传递到用例中会很麻烦。而 fixture 函数中的 autouse 参数表示用例执行时是否自动使用该函数,如果等于 True 则表示自动使用,反之等于 False 的话(autouse 默认为 False)则需要指定用例执行 fixture 函数。

这样用例就不用每次都去传参了,直接将 autouse 设置为 True,自动调用 fixture 功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#test_错误登录.py
class Test_错误密码1_1:
def test_C001001(self):
print('\n用例C001001')
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

def test_C001003(self):
print('\n用例C001003')
assert 3 == 2

class Test_错误密码1_2:

def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

#test_错误登录2.py
class Test_错误密码2:

def test_C001021(self):
print('\n用例C001021')
assert 1 == 1

def test_C001022(self):
print('\n用例C001022')
assert 2 == 2

我们在 scope 为 package 的级别情况下执行上面代码后可以发现,前置/后置的操作会在该目录下所有文件执行前后进行。

scope = 'package'

这里的 scope 一共有 4 个选项,每个选项执行的效果都不同:
- function(默认):fixture 函数只在当前测试函数中生效。
- class:fixture 函数在当前测试类中生效,所有该测试类中的测试函数都会共享这个 fixture 函数创建的资源。
- module:fixture 函数在当前测试模块中生效,所有该测试模块中的测试函数都会共享这个 fixture 函数创建的资源。
- package/session:fixture 函数在当前测试包或会话中生效,所有该测试包或会话中的测试函数都会共享这个 fixture 函数创建的资源。

为了能够更清楚的理解每个选项的作用,我们可以观察一下它们执行后的结果,其实是和之前的模块级别、类级别和方法级别有点类似的,只不过我们这里是作用于全局。

scope = 'module'

scope = 'class'

scope = 'function'

当然这里的 fixture 函数也可以当做参数传递,选择性的对用例加上前置/后置操作,但是需要注意的是这个作用域可能会随着参数的传递发生变化,这里还是来看一个 scope = ‘class’ 的例子帮助理解。这里需要注意两个点,这两个点同样适用于其它作用域:

  • 测试类中只要有一个测试用例的参数中使用了 class 级别的 fixture,则在整个测试类的所有测试用例之前都会去执行 fixture 定义的动作,以及此测试类的所有用例结束后同样要运行 fixture 指定的动作。
  • 如果在类外的函数中去使用 class 级别的 fixture,则此时在测试类外外每个测试用例中,fixture 跟 function 级别的 fixture 作用是一致的,即在类外的函数中引用了 fixture,则在此函数之前和之后同样去执行 fixture 定义的对应的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pytest

@pytest.fixture(scope="class")
def func():
a = 123
print("in fixture func before testcase......")
yield a
print("in fixture func after testcase......")

def test_01(func):
print("in outer class test_01")

def test_02():
print("in outer class test_02")

def test_03():
print("in outer class test_03")

class TestExample:
def test_01(self, func):
print("in inner class test_01")

def test_02(self, func):
print("in inner class test_02")

if __name__ == '__main__':
pytest.main(['-s', 'parse.py'])

3.3 其它参数

另外,fixture 函数中还存在三个参数:

  • params:参数化(支持列表 [],元组 (),字典列表 [{}, {}, {}],字典元组 ({}, {}, {}))
  • ids:当使用 params 参数化时,给每一个值设置一个变量名,但其实意义不大
  • name:给被 @pytest.fixture 标记的方法取一个别名,意义也不大

注意:如果取了别名,则原来的名称就不能再用了。

我们可以再看一下 params 是如何使用的,params 实际上的作用就是可以给用例传值。我们下面这个例子只让 test_C001001 方法调用 fixture 函数,来看看结果如何。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#conftest.py
import pytest

@pytest.fixture(scope='function', params=['张三', '李四', '王五'])
def st_emptyEnv(request):
return request.param

#test_错误登录.py
class Test_错误密码1_1:
def test_C001001(self, st_emptyEnv):
print('\n用例C001001 + ' + str(st_emptyEnv))
assert 1 == 1

def test_C001002(self):
print('\n用例C001002')
assert 2 == 2

可以发现 test_C001001 方法可以获取到 fixture 传入的参数,并且 params 里面有多少参数,test_C001001 方法就会执行多少次。

当然这里也同样可以将前置/后置操作一同加进来,通过 yield 来返回参数值。

1
2
3
4
5
6
7
import pytest 

@pytest.fixture(scope='function', params=['张三', '李四', '王五'])
def st_emptyEnv(request):
print(f'\n#### 初始化-目录甲')
yield request.param
print(f'\n#### 清除-目录甲')