前言:测试前后的准备清理工作
通常在测试过程中,都会包括三个步骤:测试前的准备(前置条件)-执行测试-测试后的清理。
在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 |
|
将fixture的名字通过参数直接传入测试方法即可。运行用例后,demo()
方法会先于test_demo()
执行。
通过usefixtures装饰器使用
1 | import pytest |
这样写的话,fixture无法返回参数。
多参数使用
由于fixture提供了paramas
参数,因此fixture也可以实现参数化。
1 |
|
运行结果:
1 | PASSED [ 33%]test |
可以看到,将会生成3条用例。
autouse 参数隐式使用
以上方式实现了 fixtures 和测试函数的松耦合,但是仍然存在问题:每个测试函数都需要显式声明要用哪个 fixtures。
基于此,pytest 提供了autouse 参数,允许我们在不调用 fixture 装饰器的情况下使用定义的fixture。
1 |
|
运行结果:
1 | demo.py::test_demo 调用fixture |
多 fixture 笛卡尔积使用
1 | import pytest |
将会生成3*2=6条用例:
1 | 调用fix1:a |
fixture间嵌套使用
不同的fixture间也可以嵌套使用,将fix1
作为参数传入fix2
中,如下:
1 |
|
结果:
1 | demo.py::test_demo 调用fix1 |
可以看到,当调用fix2
时,会先调用fix1。
使用 conftest.py 来共享 fixture
日常工作测试中,我们常常需要在全局范围内使用同一个测试前置操作。例如,测试开始时首先进行登录操作,接着连接数据库。
这种情况下,我们就需要使用 conftest.py。在 conftest.py 中定义的 fixture 不需要进行 import,pytest 会自动查找使用。 pytest 查找 fixture 的顺序是首先查找测试类(Class),接着查找测试模块(Module),然后是 conftest.py 文件,最后是内置或者第三方插件。
通过yield唤醒teardown
前面fixture已经帮我们实现了前置操作,那么后置如何实现呢?非常简单,通过关键字yield。
1 | import pytest |
结果:
1 | demo.py::test_demo 我是前置条件 |
通过yield,会讲该函数变为生成器,这里具体原理先不展开,简单来说,yield之前的前置条件,yield之后的为后置条件。
有返回值的fixture
大部分情况下,我都会使用fixture来返回一些值来供测试用例使用,如登录的cookie、token、数据库的连接对象等,那么fixture的返回值又是如何传递给用例的呢?如下:
1 | import pytest |
当然,也可以是多个值:
1 | import pytest |
同理,yield也能返回值。
pytest.mark.parametrize 和 pytest.fixture 结合使用
现在有一个问题,如果fixture是做了参数化的,如何在用例中动态地给它传入参数呢?
在我日常工作中,会有这么一种场景:通常我会把数据库连接放到fixture中,但是不同case中用到的数据库可能不是同一个,这就导致我会根据不同的数据库配置信息,实例多个db连接对象,那么我该如何把不同的数据库配置信息,在用例层传入给fixture呢?总不可能每个数据库都创建一个fixture吧?来看看用fixture结合parametrize是如何优雅地实现。
1 | import pytest |
首先,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的使用就如上所述了。