熟练掌握pytest 单元测试框架

1. 前言

pytest 是一个非常强大的python测试框架,熟练掌握pytest对于自动化测试工程非常重要,下面来分享一些我的使用经验

2. pytest 安装

安装命令, 要求:Python 3.8+ or PyPy3.

pip install -U pytest

安装完成可以可以通过--version来检验安装是否成功

$ pytest --version
pytest 8.3.3

3. 快速开始

pytest测试文件必须要以test_*.py或者*_test.py格式命名的文件,pytest会检查文件中所有的以test_*开头的方法,或者以Test*开头的类,通过断言来判断case是否成功。下面我们来一个简单的例子

# content of test_sample.py
def func(x):
    return x + 1


def test_answer():
    assert func(3) == 5

运行测试

pytest test_sample.py

输出结果:

============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0
rootdir: /home/ubuntu/supa
configfile: pytest.ini
collected 1 item                                                                                                                             

test_sample.py::test_answer FAILED                                                                                                     [100%]

================================================================== FAILURES ==================================================================
________________________________________________________________ test_answer _________________________________________________________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_sample.py:6: AssertionError
========================================================== short test summary info ===========================================================
FAILED test_sample.py::test_answer - assert 4 == 5
============================================================= 1 failed in 0.03s ==============================================================

通过结果的汇报我们可以很容易找到test_sample.py::test_answer 测试失败
上面展示了一个最简单的例子,在现实工程中往往不止一个case,当有多个case时,pytest可以通过::来指定运行规则

  • 运行一个指定的case
pytest tests/test_mod.py::test_func
  • 运行一个测试类中所以的case
pytest tests/test_mod.py::TestClass
  • 运行一个测试类中指定case
pytest tests/test_mod.py::TestClass::test_method
  • 运行一个特定参数的case
pytest tests/test_mod.py::test_func[x1,y2]

pytest运行时有一些常用参数,熟练掌握这些参数会让UT的调试变得非常顺利

  • -k: 会运行匹配一个关键字符的测试集,比如pytest -k hello,会打印所有带有hello的测试用例
  • -v: 打印测试进度
  • -s: 终端打印输入,当跟-v一起使用时,就可以缩写成-sv
  • -m: 运行指定标记的测试集,比如pytest -m sanity,表示运行sanity测试集
  • –junitxml: 设置输出xml结果的文件

4. 使用fixtures

使用pytest, fixture就是pytest的精髓所在,熟练掌握fixture,可以做很多setup和teardown的工作

4.1 fixture用途

  1. 做测试前后的初始化设置,如测试数据准备,链接数据库,打开浏览器等这些操作都可以使用fixture来实现
  2. 测试用例的前置条件可以使用fixture实现
  3. 与yield关键字可以实现teardown功能

4.2 fixture实战

fixture通过@pytest.fixture()装饰器装饰一个函数,那么这个函数就是一个fixture,目前常用的使用方法有三种

  1. 被@pytest.fixture()装饰的函数被作为函数参数使用
# test_fixture.py

import pytest

@pytest.fixture()
def fixtureFunc():
    return 'fixtureFunc'

def test_fixture(fixtureFunc):
    print('我调用了{}'.format(fixtureFunc))

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

执行结果

test_fixture.py .我调用了fixtureFunc
                                                        [100%]

========================== 1 passed in 0.02 seconds ===========================
Process finished with exit code 0

fixtureFunc 这个函数就是一个fixture,fixture函数内部可以实现一些初始化操作, 它被函数test_fixture作为参数进行调用, 测试中所需要的参数,会通过返回值的形式被调用
2. 使用@pytest.mark.usefixtures(‘fixture’)装饰器

# test_fixture.py
import pytest
@pytest.fixture()
def fixtureFunc():
    print('\n fixture->fixtureFunc')

@pytest.mark.usefixtures('fixtureFunc')
def test_fixture():
    print('in test_fixture')

@pytest.mark.usefixtures('fixtureFunc')
class TestFixture(object):
    def test_fixture_class(self):
        print('in class with text_fixture_class')

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

fixtureFunc被测试类TestFixture使用@pytest.mark.usefixtures(‘fixtureFunc’)进行调用
3. 设置autouse参数,实现自动调用
当设置fixture 参数autouse=True时,测试集中每个测试用例在调用时,都会先调用fixture

# test_fixture.py
import pytest
@pytest.fixture(autouse=True)
def fixtureFunc():
    print('\n fixture->fixtureFunc')

def test_fixture():
    print('in test_fixture')

class TestFixture(object):
    def test_fixture_class(self):
        print('in class with text_fixture_class')

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

运行结果

============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/supa
configfile: pytest.ini
collected 2 items                                                                                                                            

test_sample.py::test_fixture 
 fixture->fixtureFunc
in test_fixture
PASSED
test_sample.py::TestFixture::test_fixture_class 
 fixture->fixtureFunc
in class with text_fixture_class
PASSED

============================================================= 2 passed in 0.01s ==============================================================

4.3 fixture作用范围

fixture还可以通过scope参数来指定作用范围。
scope参数可以支持session,package, module,class,function五种作用范围; 默认为function

  • function: 默认范围,每个测试执行前都会调用,上面的例子都是这种级别的fixture
  • class:类级别,每个类执行前会被执行
  • module: 模块级别,模块里所有的用例执行前执行一次模块级的fixture
  • package: 包级别,当多个包时,每个包内所有用例执行前执行一次fixture
  • session: 会话级别,整个pytest 会话之前执行一次
    下面用一个简单的例子来展示一下:
# test_fixture.py
import pytest

@pytest.fixture(scope='module', autouse=True)
def module_fixture():
    print('\n-----------------')
    print('我是module fixture')
    print('-----------------')
@pytest.fixture(scope='class')
def class_fixture():
    print('\n-----------------')
    print('我是class fixture')
    print('-------------------')
@pytest.fixture(scope='function', autouse=True)
def func_fixture():
    print('\n-----------------')
    print('我是function fixture')
    print('-------------------')

def test_1():
    print('\n 我是test1')

@pytest.mark.usefixtures('class_fixture')
class TestFixture1(object):
    def test_2(self):
        print('\n我是class1里面的test2')
    def test_3(self):
        print('\n我是class1里面的test3')
@pytest.mark.usefixtures('class_fixture')
class TestFixture2(object):
    def test_4(self):
        print('\n我是class2里面的test4')
    def test_5(self):
        print('\n我是class2里面的test5')

if __name__=='__main__':
    pytest.main(['-v', '--setup-show', 'test_fixture.py'])

运行结果

============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/supa
configfile: pytest.ini
collected 5 items                                                                                                                            

test_sample.py::test_1 
-----------------
我是module fixture
-----------------

-----------------
我是function fixture
-------------------

 我是test1
PASSED
test_sample.py::TestFixture1::test_2 
-----------------
我是class fixture
-------------------

-----------------
我是function fixture
-------------------

我是class1里面的test2
PASSED
test_sample.py::TestFixture1::test_3 
-----------------
我是function fixture
-------------------

我是class1里面的test3
PASSED
test_sample.py::TestFixture2::test_4 
-----------------
我是class fixture
-------------------

-----------------
我是function fixture
-------------------

我是class2里面的test4
PASSED
test_sample.py::TestFixture2::test_5 
-----------------
我是function fixture
-------------------

我是class2里面的test5
PASSED

============================================================= 5 passed in 0.02s ==============================================================

4.4 fixture实现teardown

如果要实现teardown,我们可以与yield一起使用,我们都指定yield有类似return的功能,与之不同的是,当调用yield并不会停止执行,而是当所在函数被调出时,会继续执行,于是我们用下面一个简单的例子来展示一下:

import pytest
from selenium import webdriver
import time
@pytest.fixture()
def fixtureFunc():
  '''实现浏览器的打开和关闭'''
    driver = webdriver.Firefox()
    yield driver
    driver.quit()
def test_search(fixtureFunc):
    '''访问百度首页,搜索pytest字符串是否在页面源码中'''
    driver = fixtureFunc
    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
    
if __name__=='__main__':
    pytest.main(['--setup-show', 'test_fixture.py'])

这个实例会先打开浏览器,然后执行测试用例,最后关闭浏览器。大家可以试试! 通过yield就实现了 用例执行后的teardown功能。进一步的我们用使用module或者session 的作用域来实现整个测试的setup和teardown(爱动手的老铁相信已经开始行动了)。

5. 设置mark属性

pytest支持使用pytest.mark来轻松设置一些测试函数的metadata,使用户可以使用pytest --markers来打印所有mark属性以及描述。pytest提供了一些通过的mark属性

  • usefixtures: 前面我们用例中已经介绍过,被标记的函数会使用相应的testure
  • filterwarnings: 过滤测试函数的所有警告信息
  • skip: 跳过当前测试用例
  • skipif: 如果条件为true则跳过测试用例
  • xfail: 如果特定条件发生,会发生一个failure的异常
  • parametrize: 参数化,常用于参数测试
    以上的mark属性可能不能满足用户需求,如果想使用自己的mark属性,则需要通过pytest.ini进行注册,例如:
# pytest.ini
[pytest]
markers =
    ci_mini: Run ci_mini case
    sanity: Run pr case
    regression: Run nightly case

上面注册了ci_mini、sanity、regression等三种属性,平时可以用来测试三种等级的测试,ci_mini最基础的测试,用于一些流程的打通测试,sanity可以用于MR的门禁测试, regression则可以用于大规模压测。下面使用一个例子来展示一下用法

import pytest

@pytest.mark.skip
@pytest.mark.sanity
def test_fun1():
    print('in test_fun1')
    return False

@pytest.mark.ci_mini
@pytest.mark.sanity
def test_fun2():
    print('in test_fun2')
    return True

@pytest.mark.sanity
def test_fun3():
    print('in test_fun3')
    return True

@pytest.mark.regression
def test_fun4():
    print('in test_fun4')
    return True

从用例可以看到mark属性是可以叠加的。我们可以使用pytest -m来指定要运行mark属性

pytest -m sanity -sv test_sample.py

运行结果

============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/supa/build
configfile: pytest.ini
collected 4 items / 1 deselected / 3 selected                                                                                                

test_sample.py::test_fun1 SKIPPED (unconditional skip)
test_sample.py::test_fun2 in test_fun2
PASSED
test_sample.py::test_fun3 in test_fun3
PASSED

============================================================== warnings summary ==============================================================
test_sample.py::test_fun2
  /home/ubuntu/.local/lib/python3.8/site-packages/_pytest/python.py:163: PytestReturnNotNoneWarning: Expected None, but test_sample.py::test_fun2 returned True, which will be an error in a future version of pytest.  Did you mean to use `assert` instead of `return`?
    warnings.warn(

test_sample.py::test_fun3
  /home/ubuntu/.local/lib/python3.8/site-packages/_pytest/python.py:163: PytestReturnNotNoneWarning: Expected None, but test_sample.py::test_fun3 returned True, which will be an error in a future version of pytest.  Did you mean to use `assert` instead of `return`?
    warnings.warn(

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================================== 2 passed, 1 skipped, 1 deselected, 2 warnings in 0.02s ===========================================

6. 参数化

上面我们提到,可以通过mark属性pytest.mark.parametrize来设置测试的参数,需要注意的是传入的参数必须是一个list,list中的每个元素会通过参数传入, 例如

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

运行结果

============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/supa/build
configfile: pytest.ini
collected 3 items                                                                                                                            

test_sample.py::test_eval[3+5-8] PASSED
test_sample.py::test_eval[2+4-6] PASSED
test_sample.py::test_eval[6*9-42] FAILED

================================================================== FAILURES ==================================================================
_____________________________________________________________ test_eval[6*9-42] ______________________________________________________________

test_input = '6*9', expected = 42

    @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
E       AssertionError: assert 54 == 42
E        +  where 54 = eval('6*9')

test_sample.py:5: AssertionError
========================================================== short test summary info ===========================================================
FAILED test_sample.py::test_eval[6*9-42] - AssertionError: assert 54 == 42
======================================================== 1 failed, 2 passed in 0.03s =========================================================

7. 并行运行

默认情况下,pytest都是按顺序运行测试,但是在真实工程环境中,往往一个测试套会有很多的测试文件,每个文件都会有一堆测试,顺序执行会导致执行效率较低,为了解决这个问题,pytest为我们提供了并行测试的选项。
如果想使用并行测试,需要先安装 pytest-xdist 插件,安装指令如下:

pip install pytest-xdist

安装成功后,我们就可以使用语法pytest -n <num>来并行运行,这里的num指定是使用的worker数量,比如使用3个work

pytest -n 3

8. XML格式输出测试结果

默认情况下,pytest只会在终端打印测试结果,如果想保存测试结果,只需要使用 --junitxml=, 例如:

pytest test_sample.py -v --junitxml="result.xml"

表示保存test_sample.py 的测试结果到到result.xml中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值