使用 pytest
pytest 这个 库是一个第三方库,严格来说,它的设计思路不属于 xUnit 系列。但它使用起来比较方便,同时他又兼容 unittest 的用例:用 unittest 写的测试脚本可以用 pytest 来执行。
这种兼容性的设计,在测试执行器的设计思路层面上很普遍。举个例子,几乎所有测试执行器,都兼容 junit 的测试报告,他们都可以输出一种 最初由 junit 提供的 xml 测试报告(有些测试执行器是原生自带这个功能,有些是用插件实现这个功能)。兼容现有工具,有利于新工具的推广,因此各种比较知名的测试执行器都会从设计上就考虑兼容性。
言归正传,接下来介绍 测试执行器的几个重点功能,以及我们怎样用 pytest 里的这些功能。
任务1.安装 pytest
和其他python 库一样我们使用 pip install pytest 来安装pytest
任务2.打开官方文档
测试命名规范
pytest 里,测试用例的定义较 unittest 做了简化。
1.类名规范取消,不用继承任何类
上一节的例子中,
我们使用unittest时,需要把测试写在类里,这个类还必须继承 unittest.TestCase 像这样:
class TestStringMethods(unittest.TestCase):
只有继承了 unittest.TestCase 这个类,unittest 才能找到这个类里我们写的测试方法。
而pytest 里,不再强制要求把测试写在类里,也不需要继承任何类。
取而代之的,是使用文件名规范来让pytest 找到我们写的测试方法的文件。
2.文件名以 test_ 开头,注意带下划线
例1.test_1540.py,一个pytest的例子。注意文件名。
import pytestdef inc(x): return x + 1def test_answer(): assert inc(3) == 5if __name__ == '__main__': pytest.main()
我们一起来看一下这个例子:
首先第一行,导入 pytest 库
第2-3 行,定义了一个 inc 方法,这个方法会把传入参数加1,再返回。
第4-5 行,定义了一个 test 方法,这一点和unittest 一样,测试方法名要以 test 开头
第6-7行, 定义了程序的入口,这两行可以省略
断言
官方文档中告诉我们,pytest的断言里只要用assert 就行了,不需要 self.assertXXXX。
pytest 会显示这样 的错误信息给我们,以下为例1的运行结果:
============================FAILURES===============
_______________________________ test_answer ________________________________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_1540.py:7: AssertionError
=================== 1 failed in 0.07 seconds=================
并且上一节中我们用过的自定义更详细的错误信息的方法在这里仍然适用。
setup和teardown
所谓 setup 和 teardown,也是 xUnit 系列测试执行器中的概念。比如,假设我们有3个测试方法,都是操作在线购物网站的购物车的测试,他们有一个共同的前提条件,就是用户需要先登录。那么通常在 xUnit 系列的测试执行器中,测试脚本我们会这样写:
上图示意了一个带有 setup 和teardown 操作的测试套件(test suite)的内部逻辑,这里的 setup 和teardown 是对测试套件的,同样也可以定义对 Case 的和对测试方法的 setup 和teardown 。
当执行一个测试套件时,其顺序是:
套件的 steup====》
case1 的setup====》 case1 的测试方法====》 case1的teardown ====》
case2 的setup====》 case2 的测试方法====》 case2的teardown ====》
case3 的setup====》 case3 的测试方法====》 case3的teardown ====》
套件的 teardown
在pytest中,也支持上述的传统 setup 和 teardown,感兴趣的同学可以看官方文档:
https://docs.pytest.org/en/latest/xunit_setup.html#xunitsetup
本文中,将介绍 pytest 的fixture 以及用fixture实现的 setup和 teardown
Pytest的Fixture
fixture 是什么?
我们可以理解成 fixture 是提供给测试方法用的提前准备好的对象。
举个例子,我们做网页测试,需要先打开一个浏览器,后续所有操作都是在这个浏览器上做的。 fixture 能做的就是给我们的每个测试方法,都准备好一个浏览器对象。
同样,我们做一些测试时,需要先读取一个 excel表格,然后所有测试方法,都需要用这个表格里的某些数据,那么fixture能做的就是给每个测试方法,都准备好一个已经读取完毕的 excel 表格对象。
我们一起来看一个官网的例子:
例2.官网给的 fixture 例子
# content of conftest.py
import pytest import smtplib
@pytest.fixture(scope="module") def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
# content of test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
assert 0 # for demo purposes
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
这个例子里涉及了 conftest.py 和 test_module.py 两个文件
在conftest.py 中,定义了一个 smtp_connection 方法,这个方法使用 smtplib 这个库去建立了一个 gmail 的链接,也就是这两行
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
而 @pytest.fixture(scope="module") 这一行表示后面紧跟的 smtp_connection 方法是一个fixutre,并且范围是整个module。 范围是 module,则表示这个fixture 在每个module 只会运行一次。在这里,module的 概念和测试套件差不多。本例中,整个 module 也就只有两个测试方法。 也就是说:
这个 smtp_connection 方法在这次整个测试中只会被执行一次。换句话说,它就相当于是 整个测试套件的 setup 方法了。
在 test_module.py 中,定义了两个测试方法,这两个测试方法的共同点是,传入参数里都有 smtp_connection。 没错,这里的smtp_connection就是 conftest中的 smtp_connection 的返回值。
虽然官网的例子我们无法运行,但是我们可以一起来回顾一下整个测试执行过程:
1.先找到所有 test_开头的文件,称为测试脚本文件
2.在测试脚本文件同一级目录下寻找conftest.py,称为测试配置文件
3.按随机顺序执行测试脚本文件中的测试方法
4.执行第一个测试方法,发现有一个传入参数 smtp_connection,在测试配置文件中寻找名为 smtp_connection 的fixture
5.执行测试配置文件中的 smtp_connection 方法,保存返回值
6.把上一步的返回值代入第4步的测试方法传入参数中,执行第一个测试方法
7.执行第二个测试方法,发现有一个传入参数 smtp_connection ,在测试配置文件中寻找名为 smtp_connection 的fixture
8.发现这个fixture的范围是module,无需重复执行,使用第5步的返回值继续执行第7步的第二个测试方法。
理解了上述流程,我们发现, fixture 其实就相当于是 setup方法,并且更灵活:
通过修改 fixture 的 scope (它的值可以是 module,class或fucntion)我们可以给每个方法、每个类定制不同的fixture。 同样,fixture其实也可以定义teardown方法。
例3.在官网例子上增加 teardown
@pytest.fixture(scope="module")
def smtp_connection():
yield smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
print("我就是 teardown,我在测试方法结束后运行")
这个例子中,第四行的 return 改成了 yield。而 第五行开始的内容就会在测试方法执行结束后运行了。相当于是实现了teardown。
下面一起看一个可以运行的例子:
例4.一个用fixture 实现测试方法级别的setup和teardown的例子:
#conftest.py的内容
import pytest
@pytest.fixture(scope="function",autouse=True)
def foo():
print(" function setup")
yield 100
print(" function teardown")
#test_1540.py的内容
import pytest
def inc(x):
return x + 1
def test_answer_1():
assert inc(3) == 5
def test_answer_2(foo):
print(foo)
assert inc(98) == foo
if __name__ == '__main__':
pytest.main()
一起来看一下这个例子,
在文件conftest.py里,
第2行,使用了装饰器 pytest.fixture,这个装饰器带的参数值表示这个fixture的生效范围是方法级(scope= “function”),也就是说每个方法之前之后都会运行它。并且会自动使用(autouse=True),这个自动使用为真时,我们在测试方法的传入参数表里可以省略这个 fixture的方法名。当然,如果在传入参数里省略了foo,那么就无法使用 foo的返回值。所以一般要自动使用的fixture都是没有返回值的。
第3-6行定义了这个fixture foo,并且返回值固定为100。 返回值 使用 yield 来返回,这样 yield 后的语句会在 测试方法执行后被执行。
运行这个例子的结果如下:
rootdir: C:\Users\colin.zt\Desktop\Homework_20180914\15.0, inifile:
collected 2 items
test_1540.py FF [100%]
================ FAILURES==========================
________________________________ test_answer_1 ________________________________
def test_answer_1():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_1540.py:9: AssertionError
---------------------------- Captured stdout setup ----------------------------
function setup
-------------------------- Captured stdout teardown ---------------------------
function teardown
________________________________ test_answer_2 ________________________________
foo = 100
def test_answer_2(foo):
print(foo)
> assert inc(98) == foo
E assert 99 == 100
E + where 99 = inc(98)
test_1540.py:14: AssertionError
---------------------------- Captured stdout setup ----------------------------
function setup
---------------------------- Captured stdout call -----------------------------
100
-------------------------- Captured stdout teardown ---------------------------
function teardown
========== 2 failed in 0.08 seconds ================
其中需要说明的部分是:Captured stdout setup 和 Captured stdout teardown 这两部分是 pytest 抓取的 setup 和teardown部分的日志,其内容是我们在 foo方法里输出的内容。可以看到上述结果中,共抓到了两次 setup和两次 teardown,这是因为我们的foo方法 范围是 function,而我们有两个测试方法。因此每个测试方法前后都会执行 foo方法的对应语句。
另外,def test_answer_1(): 这个方法里没有显式传入参数 foo,但因为 foo的autouse = True,所以test_answer_1方法执行前后也会执行 foo 方法。
而 def test_answer_2(foo):里显式传入了参数 foo,那么除了执行foo 方法以外,传输参数 foo 还会带有foo 方法的返回值,即 100。