前言:测试前后的准备清理工作

通常在测试过程中,都会包括三个步骤:测试前的准备(前置条件)-执行测试-测试后的清理。

在unittest框架中,通常使用setup/teardown来完成测试的前置和后置操作。

在pytest框架中,也有类似的方法来完成对应的操作,如使用 setup_method、setup_class、setup_module 来分别完成测试类方法、测试类,以及测试 module 的 setup;;使用 teardown_method、teardown_class、teardown_module 来分别完成测试类方法、测试类,以及测试 module 清理操作。

但是这种方式存在缺陷。 例如,在同一个测试类中,存在多个测试方法,假设每一个测试方法需要不同的 setup 或者 teardown 函数,此时该怎么办呢?

又如,这些前后置操作,能放到一个统一模块去管理么?

答案是肯定的,pytest提供了一种更高级的功能,fixture装饰器

fixture装饰器可以非常方便的自定义各种前置后置方法供测试用例使用,而且可以通过conftest.py文件进行共享,供其他函数、模块、类或者整个项目使用。

1、fixture语法

1
fixture(scope="function", params=None, autouse=False, ids=None, name=None)

fixture提供了5个参数。

scope:控制fixture的作用域

scpoe有4个级别,分别是:

function:在每一个function或者类方法中都会调用(默认)。

class:在每一个类中只调用一次。

module:在每一个.py 文件调用一次。

session:一个session调用一次,如运行整个项目有100条用例,那么本次用例执行过程中只会调用一次。

params:一个可选的参数列表

params 以可选的参数列表形式存在。在测试函数中使用时,可通过 request.param 接收设置的返回值(即 params 列表里的值)。params 中有多少元素,在测试时,引用此 fixture 的函数就会调用几次。

autouse:是否自动执行设置的 fixtures

当 autouse 为 True 时,测试函数即使不调用 fixture 装饰器,定义的 fixture 函数也会被执行。

ids:指定每个字符串 id

当有多个 params 时,针对每一个 param,可以指定 id,这个 id 将变为测试用例名字的一部分。如果没有提供 id,则 id 将自动生成。

name:fixture 的名称

name 是 fixtures 的名称, 它默认是你装饰的那个 fixture 函数的名称。可以通过 name 参数来更改这个 fixture 名称,更改后,如果这个 fixture 被调用,则使用更改后的名称即可。

2、fixture 用法

通过函数名直接使用

1
2
3
4
5
6
@pytest.fixture()
def demo():
print(f'调用fixture')

def test_demo(demo):
print('test')

将fixture的名字通过参数直接传入测试方法即可。运行用例后,demo()方法会先于test_demo()执行。

通过usefixtures装饰器使用

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

@pytest.fixture()
def demo():
print(f'调用fixture')

@pytest.mark.usefixtures('demo')
def test_demo(demo):
print('test')

@pytest.mark.usefixtures('demo')
class TestDemo:
def test_demo_1(self):
pass
def test_demo_2(self):
pass

这样写的话,fixture无法返回参数

多参数使用

由于fixture提供了paramas参数,因此fixture也可以实现参数化。

1
2
3
4
5
6
@pytest.fixture(params=['a', 'b', 'c'])
def demo(request):
print(f'调用fixture:{request.param}')

def test_demo(demo):
print('test')

运行结果:

1
2
3
4
5
PASSED                                             [ 33%]test
调用fixture:b
PASSED [ 66%]test
调用fixture:c
PASSED [100%]test

可以看到,将会生成3条用例。

autouse 参数隐式使用

以上方式实现了 fixtures 和测试函数的松耦合,但是仍然存在问题:每个测试函数都需要显式声明要用哪个 fixtures。

基于此,pytest 提供了autouse 参数,允许我们在不调用 fixture 装饰器的情况下使用定义的fixture。

1
2
3
4
5
6
@pytest.fixture(autouse=True)
def demo():
print(f'调用fixture')

def test_demo():
print('test')

运行结果:

1
2
demo.py::test_demo 调用fixture
PASSED [100%]测试

多 fixture 笛卡尔积使用

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

@pytest.fixture(params=['a', 'b', 'c'])
def fix1(request):
print(f'调用fix1:{request.param}')

@pytest.fixture(params=[1, 2])
def fix2(request):
print(f'调用fix2:{request.param}')

def test_demo(fix1, fix2):
print('-----我是分割线------')

将会生成3*2=6条用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
调用fix1:a
调用fix2:1
PASSED [ 16%]-----我是分割线------
调用fix1:a
调用fix2:2
PASSED [ 33%]-----我是分割线------
调用fix1:b
调用fix2:1
PASSED [ 50%]-----我是分割线------
调用fix1:b
调用fix2:2
PASSED [ 66%]-----我是分割线------
调用fix1:c
调用fix2:1
PASSED [ 83%]-----我是分割线------
调用fix1:c
调用fix2:2
PASSED [100%]-----我是分割线------

fixture间嵌套使用

不同的fixture间也可以嵌套使用,将fix1作为参数传入fix2中,如下:

1
2
3
4
5
6
7
8
9
10
@pytest.fixture()
def fix1():
print('调用fix1')

@pytest.fixture()
def fix2(fix1):
print('调用fix2')

def test_demo(fix2):
print('-----我是分割线------')

结果:

1
2
3
demo.py::test_demo 调用fix1
调用fix2
PASSED [100%]-----我是分割线------

可以看到,当调用fix2时,会先调用fix1。

使用 conftest.py 来共享 fixture

日常工作测试中,我们常常需要在全局范围内使用同一个测试前置操作。例如,测试开始时首先进行登录操作,接着连接数据库。

这种情况下,我们就需要使用 conftest.py。在 conftest.py 中定义的 fixture 不需要进行 import,pytest 会自动查找使用。 pytest 查找 fixture 的顺序是首先查找测试类(Class),接着查找测试模块(Module),然后是 conftest.py 文件,最后是内置或者第三方插件。

通过yield唤醒teardown

前面fixture已经帮我们实现了前置操作,那么后置如何实现呢?非常简单,通过关键字yield

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

@pytest.fixture()
def fix():
print('我是前置条件')
yield
print('我是后置条件')

def test_demo(fix):
print('-----我是分割线------')

结果:

1
2
3
demo.py::test_demo 我是前置条件
PASSED [100%]-----我是分割线------
我是后置条件

通过yield,会讲该函数变为生成器,这里具体原理先不展开,简单来说,yield之前的前置条件,yield之后的为后置条件。

有返回值的fixture

大部分情况下,我都会使用fixture来返回一些值来供测试用例使用,如登录的cookie、token、数据库的连接对象等,那么fixture的返回值又是如何传递给用例的呢?如下:

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

@pytest.fixture()
def fix():
a = 1 + 1
return a

def test_demo(fix):
# 接收fix值传递给了变量x
x= fix
print(f'这是我接收到fix的变量:{x}')

当然,也可以是多个值:

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

@pytest.fixture()
def fix():
a = 1 + 1
b = 2 + 2
c = 3 + 3
return a,b,c

def test_demo(fix):
# 接收fix值传递给了变量x,y,z
# 如果只用一个变量接收,类型将会是元组
x,y,z = fix
print(f'这是我接收到fix的变量:{x}{y}{z}')

同理,yield也能返回值。

pytest.mark.parametrize 和 pytest.fixture 结合使用

现在有一个问题,如果fixture是做了参数化的,如何在用例中动态地给它传入参数呢?

在我日常工作中,会有这么一种场景:通常我会把数据库连接放到fixture中,但是不同case中用到的数据库可能不是同一个,这就导致我会根据不同的数据库配置信息,实例多个db连接对象,那么我该如何把不同的数据库配置信息,在用例层传入给fixture呢?总不可能每个数据库都创建一个fixture吧?来看看用fixture结合parametrize是如何优雅地实现。

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

@pytest.fixture(scope='class')
def connect_db(request):
db = HandleMysql(request.param)
yield db
db.close()

class TestDemo:
# db1,db2,db3是伪代码,表示数据库不同的连接配置信息
@pytest.mark.parametrize('connect_db', [db_conf], indirect=True)
def test_demo(self,connect_db):
# 接收fix值传递给了变量x
db = connect_db
print(f'这是我的数据库连接对象:{connect_db}')

首先,fixture做了参数化,那么就需要在fixture中接受变量,传入request这个内置fixture,然后传入的变量,通过request的param接收,这是需要再fixture内部做的操作。

然后,在用例层的**@pytest.mark.parametrize**中,将配置信息变量db_conf传入到connect_db这个fixture中,需要注意的是,这里必须设置参数indirect=True

当indirect为True的时候,变量为固件函数名称的,执行的时候会将变量(此例中即为connect_db)当做函数来执行。

当indirect为false的时候,变量为固件函数名称的,执行的时候会将变量当做一个参数来执行。

ok,日常项目中,关于fixture的使用就如上所述了。