Python单测框架Pytest教程

简介: The pytest framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries. pytest介绍和教程。pytest 框架使编写小型可读测试变得容易,并且可以扩展以支持应用程序和库的复杂功能测试。

1. What: 单测框架

1.1 什么是单测

单元测试是自动化测试的一种形式--这仅仅意味着测试计划是由一个脚本执行的,而不是由人手动执行的。
它们作为软件测试的第一层,通常以函数的形式编写,验证软件程序中各种功能的行为。

1.2 为什么要写单元测试

以下列举了一些我为什么使用单元测试的好处:

  1. 减少bug:允许你对代码做出任何改变,因为你了解单元测试会在你的预期之中。单元测试可以有效地降低程序出现BUG的机率。
  2. 重构:帮助你更深入地理解代码--因为在写单元测试的时候,你需要明确程序所有的执行流程及对应的执行结果等等; 允许在任何时候代码重构,而不必担心破坏现有的代码。这使得我们编写程序更灵活,确保你的代码的健壮性,因为所有的测试都是通过了的。
  3. 文档记录:单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
  4. 回归性:自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。

2. Why: 为什么是Pytest

官网上的pytest:
helps you write better programs.
The pytest framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries.

测试是如此重要,以至于Python带有自己的内置测试框架,称为unittest。但是,在unittest中编写测试可能很复杂,因此近年来,pytest框架已成为标准。

  1. 非常容易開始,因為它的簡單和容易的語法。
  2. 可以並行執行測試。
  3. 可以執行特定測試或測試子集
  4. 自動檢測測試
  5. 跳過測試
  6. 開源
  7. 使用普通的断言语句而不是 unittest 的 assertSomething 方法(例如,assertEquals、assertTrue)
  8. 简化了测试状态的设置和拆卸

3. How:Pytest教程

3.1 安装

pip install -U pytest
pip install -U pytest-html
pip install -U pytest-rerunfailures

3.2 一个简单的例子

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

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

执行测试:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_sample.py F                                                     [100%]

================================= FAILURES =================================
_______________________________ test_answer ________________________________

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

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

执行测试的时候,我们只需要在测试文件testsample所在的目录下,运行pytest即可。
pytest会在当前的目录下,寻找以test开头的文件(即测试文件),找到测试文件之后,进入到测试文件中寻找test
开头的测试函数并执行。
通过上面的测试输出,我们可以看到该测试过程中,一个收集到了一个测试函数,测试结果是失败的(标记为F),并且在FAILURES部分输出了详细的错误信息,帮助我们分析测试原因。
我们可以看到”assert func(3) == 5”这条语句出错了,错误的原因是func(3)=4,然后我们断言func(3) 等于 5。

3.3 如何编写pytest测试

3.3.1 规则

3.3.1.1 编写规则

pytest执行单测需要按照下面的规则:

  1. 测试文件以test_开头(以_test结尾也可以)
  2. 测试类以Test开头,并且不能带有 init 方法
  3. 测试函数以test_开头
  4. 断言使用基本的assert即可

3.3.1.2 执行规则

执行测试:

pytest               # 在当前目录下执行所有测试用例
pytest test_mod.py   # 执行指定文件中的所有测试用例
pytest somepath      # 执行指定目录下的所有测试用例
pytest -k stringexpr # 执行匹配到字符串的测试用例
pytest test_mod.py::test_func # 执行指定文件中的指定测试用例

3.3.2 测试函数

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

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

3.3.3 测试类

# content of test_class.py
class TestClass:
    def test_one(self):
        x = "this"
        assert 'h' in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, 'check')

执行测试:

$ pytest -q test_class.py
.F
================================= FAILURES =================================
____________________________ TestClass.test_two ____________________________
self = <test_class.TestClass object at 0x7fbf54cf5668>
def test_two(self):
x = "hello"
> assert hasattr(x, 'check')
E assert hasattr('hello', 'check')
test_class.py:8: AssertionError
1 failed, 1 passed in 0.01 seconds

pytest -q 可以不在输出版本信息。
该测试共执行了两个测试样例,一个失败一个成功。同样,我们也看到失败样例的详细信息,和执行过程中的中间结果。
pytest中用点号(.)表示一条用例被执行并通过,用F来标识一条用例被执行并失败。

如果想查看详情可以在pytest命令后面加上-v或者–verbose选项:

$ pytest -v test_class.py

collected 2 items

test_class.py::TestClass::test_one PASSED                                                                        [ 50%]
test_class.py::TestClass::test_two FAILED                                                                        [100%]

======================================================= FAILURES =======================================================
__________________________________________________ TestClass.test_two __________________________________________________

self = <test_class.TestClass object at 0x7fbcb0a15030>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, 'check')
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

test_class.py:10: AssertionError

3.3.4 使用raises在单测中捕获异常

有时候我们在做测试的时候,预期就是抛出一个异常,但如果在正常情况下,抛出异常后pytest或停止该条测试用例的继续执行,所以我们需要一个期望抛出异常的方法,pytest.raises就可以做到这一点,代码如下:

# content of test_sysexit.py
import pytest

def f():
    raise SystemExit(1)

def test_mytest():
    # 此处必须抛出SystemExit的异常,用例才会通过
    with pytest.raises(SystemExit):
        f()

def test_add_raises():
    with pytest.raises(AssertionError):
        # 此处必须抛出AssertionError的异常,用例才会通过
        assert 1 + 1 == 3

3.3.5 使用固件(fixture)

固件(Fixture)是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们。
固件是为了最大复用代码,以及在固定流程中注入代码使用。

3.3.5.1 使用

在函数声明之前加上“@pytest.fixture”。其他函数要来调用这个Fixture,只用把它当做一个输入的参数即可。

import pytest

@pytest.fixture()
def before():
    print('\nbefore each test')

def test_1(before):
    print('test_1()')

def test_2(before):
    print('test_2()')
    assert 0

除了在函数参数调用外,还可以显示指定调用,下面三种方式都是一样的功能:

import pytest

# 固件定义
@pytest.fixture()
def before():
    print('\nbefore each test')

# 函数前指定
@pytest.mark.usefixtures("before")
def test_1():
    print('test_1()')

@pytest.mark.usefixtures("before")
def test_2():
    print('test_2()')

class Test1:
    # 类的成员函数前指定
    @pytest.mark.usefixtures("before")
    def test_3(self):
        print('test_4()')

    @pytest.mark.usefixtures("before")
    def test_4(self):
        print('test_4()')

# 类的定以前指定
@pytest.mark.usefixtures("before")
class Test2:
    def test_5(self):
        print('test_5()')

    def test_6(self):
        print('test_6()')

除了上述定在一个指定函数外,还可以文件conftest.py集中管理固件(pytest会自动调用)。

# conftest.py
import pytest

@pytest.fixture()
def data():
    return 3

# test_fun.py
# 测试成功
def test_pass(data):
    assert func(data) == 4

Pytest 使用 yield 关键词将固件分为两部分,yield 之前的代码属于预处理,会在测试前执行;yield 之后的代码属于后处理,将在测试完成后执行。

# conftest.py
import pytest

@pytest.fixture()
def db():
    print('opened')
    yield
    print('closed')

3.3.5.2 固件的作用范围

fixture的scope参数用来作用范围,scope参数有四种,’function’,’module’,’class’,’session’,默认为function。

  1. function:每个test都运行,默认是function的scope
  2. class:每个class的所有test只运行一次
  3. module:每个module的所有test只运行一次
  4. session:每个session只运行一次
    ```python

    conftest.py

    import pytest

@pytest.fixture(scope='function', autouse=True)
def func_scope():
print('opened')
yield
print('closed')

@pytest.fixture(scope='module', autouse=True)
def mod_scope():
pass

@pytest.fixture(scope='session')
def sess_scope():
pass

@pytest.fixture(scope='class')
def class_scope():
pass


```python
# test_fun.py 
import pytest

@pytest.mark.usefixtures('sess_scope')
def test_pass(class_scope):
    assert func(3) == 4

输出结果:

# 使用 --setuo-show 选项,显示详细的固件信息
$ pytest --setup-show -k pass test_fun.py
collected 2 items / 1 deselected / 1 selected                                                                                                                            

test_sample.py
SETUP    S sess_scope
    SETUP    M mod_scope
      SETUP    C class_scope
        SETUP    F func_scope
        test_sample.py::test_pass (fixtures used: class_scope, func_scope, mod_scope, sess_scope).
        TEARDOWN F func_scope
      TEARDOWN C class_scope
    TEARDOWN M mod_scope
TEARDOWN S sess_scope

setup和teardown操作:

  1. setup,在测试函数或类之前执行,完成准备工作,例如数据库链接、测试数据、打开文件等
  2. teardown,在测试函数或类之后执行,完成收尾工作,例如断开数据库链接、回收内存资源等
  3. 备注:也可以通过在fixture函数中通过yield实现setup和teardown功能

4. One More Thing

4.1 重复运行

pip install pytest-repeat

# 在执行命令中添加–count=NUM NUM为每条用例需要执行的次数
pytest test_se.py --count=3

4.2 并行运行

pip install pytest-xdist

# pytest-xdist插件的 -n numprocesses 选项可以指定运行测试的处理器进程数,-n auto选项可以自动侦测系统里的CPU数码
pytest -n 3 test_test.py
pytest -n auto test_test.py

4.3 设定执行顺序

pip install pytest-ordering
# 根据order参数从小到大执行

@pytest.mark.run(order=2)
def test_order1():
    print ("first test")
    assert True

@pytest.mark.run(order=1)
def test_order2():
    print ("second test")
    assert True

4.4 测试时间限制

在正常时间下,pytest是没有测试时间限制的,但有时候需要控制测试用例执行执行,可以使用pytest-timeout。

pip install pytest-timeout

# 在命令行中添加 –timeout=X,X为该条命令执行的总时间限制,单位秒
pytest --timeout=3.5 test_xxx.py

4.5 常用执行参数

-m: 用表达式指定多个标记名。 pytest 提供了一个装饰器 @pytest.mark.xxx,用于标记测试并分组(xxx是你定义的分组名),以便你快速选中并运行,各个分组直接用 and、or 来分割。
# 首先给测试用例打标签(mark),在 Class、method 上加上如下装饰器:
# @pytest.mark.xxx

# 同时选中带有这两个标签的所有测试用例运行
pytest -m "mark1 and mark2"
# 选中带有mark1的测试用例,不运行mark2的测试用例
pytest -m "mark1 and not mark2"
# 选中带有mark1或 mark2标签的所有测试用例
pytest -m "mark1 or mark2"

-v: 运行时输出更详细的用例执行信息 不使用 -v 参数,运行时不会显示运行的具体测试用例名称;使用 -v 参数,会在 console 里打印出具体哪条测试用例被运行。

-q: 类似 unittest 里的 verbosity,用来简化运行输出信息。 使用 -q 运行测试用例,仅仅显示很简单的运行信息, 例如:
.s..  [100%]
3 passed, 1 skipped in 9.60s

-k: 可以通过表达式运行指定的测试用例。 它是一种模糊匹配,用 and 或 or 区分各个关键字,匹配范围有文件名、类名、函数名。
# 运行test_se.py下的所有的测试
pytest -k "test_se.py"
# 因为se能匹配上test_se.py,故运行test_se.py下所有的测试
pytest -k "se"
# 因为Baidu能匹配上test_baidu.py里定义的测试类Baidu,故运行Baidu测试类下所有的测试,你也可以写成Bai.
pytest -k "Baidu"


-x: 出现一条测试用例失败就退出测试。 在调试时,这个功能非常有用。当出现测试失败时,停止运行后续的测试。

-s: 显示print内容 在运行测试脚本时,为了调试或打印一些内容,我们会在代码中加一些print内容,但是在运行pytest时,这些内容不会显示出来。如果带上-s,就可以显示了。
pytest test_se.py -s

4.6 忽略测试用例执行

4.6.1 直接忽略测试执行

# 直接忽略可以使用 @pytest.mark.skip 装饰器来实现。
@pytest.mark.skip(reason='skip此测试用例')
def test_get_new_message:
    pass

4.6.2 按条件忽略测试执行—使用’skipif’忽略

# 按skipif条件,当条件符合时便会忽略某条测试用例执行。
# 定义一个flag,用来指示是否要skip一个测试用例
flag = 1
# 此处判断flag的值,为1则忽略,0则不忽略 
@pytest.mark.skipif(flag == 1, reason='by condition')
def test_get_new_message:
    pass

4.6.3 xfail 失败后继续执行

在使用pytest时,有些用例我们预计他可能会失败,这个时候就需要使用xfail了,当测试用例失败时,会被跳过:

import pytest

@pytest.mark.xfail()
def test_1():
    assert 1 == 2

@pytest.mark.xfail()
def test_2():
    assert 1 == 1

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

执行后的结果,会使用大小写的xX来表示,算入预期内的通过和不通过。

4.6.4 按条件忽略测试执行—使用’-m’或者’-k’忽略

# 忽略方法名包含method的测试用例
pytest -k "not method"
# 忽略被装饰器@pytest.mark.slow装饰的所有测试用例
pytest -m "not slow"

5. Author: 作者信息

数字老K
quantgalaxy@outlook.com
欢迎交流

目录
相关文章
|
10天前
|
数据采集 存储 JSON
Python网络爬虫教程概览
【6月更文挑战第21天】Python网络爬虫教程概览:安装requests和BeautifulSoup库抓取网页;使用HTTP GET请求获取HTML,解析标题;利用CSS选择器提取数据;处理异步内容可选Selenium;遵循爬虫策略,处理异常,尊重法律与网站规定。
18 1
|
4天前
|
Shell Python
Python教程:return和yield的区别
Python教程:return和yield的区别
6 0
Python教程:return和yield的区别
|
6天前
|
数据采集 存储 数据处理
使用Python获取1688商品详情的教程
使用Python爬取1688商品详情,涉及requests库抓取页面、BeautifulSoup解析HTML,安装必要库如requests、beautifulsoup4、pandas和lxml。通过get_page发送请求,BeautifulSoup解析提取如标题、价格等信息。数据处理后可使用pandas保存至CSV。注意遵守法律法规和网站政策,避免频繁请求。[代码片段及更多详情见链接
|
12天前
|
存储 数据安全/隐私保护 计算机视觉
Python教程:一文了解从Bytes到Bits的数据转换
在Python编程中,处理数据时经常需要在字节(bytes)和位(bits)之间进行转换。这种转换在网络通信、数据加密、图像处理等领域尤为常见。本文将详细介绍如何在Python中进行字节与位之间的转换,并提供一个实用的功能:如何在指定的位位置替换位数据。
23 4
|
10天前
|
缓存 前端开发 API
了解python中几个主流的网络框架
【6月更文挑战第21天】探索Python Web几个流行框架,了解各框架特性以适应不同场景需求。
29 1
|
10天前
|
存储 JSON 数据格式
Python基础语法汇总【保姆级小白教程】
我将 Python语法分为14个章节,从第一章Python基础概念到第14章模块&异常处理,本篇文章将逐一为大家讲述.
41 0
Python基础语法汇总【保姆级小白教程】
|
12天前
|
设计模式 Python
Python教程:一文了解Python工厂模式
工厂模式是一种创建型设计模式,它用于创建对象的实例,而无需在客户端代码中指定具体的类。通过引入工厂类,客户端代码只需要与工厂接口进行交互,而不需要了解实际创建的对象的细节。这样可以将对象的创建和使用分离开来,提高了代码的灵活性和可维护性。
15 2
|
2天前
|
机器人 API 开发者
Python基于Mirai开发的QQ机器人保姆式教程(亲测可用)
Python基于Mirai开发的QQ机器人保姆式教程(亲测可用)
|
2天前
|
数据采集 XML 存储
自动核对名单详细教程〖Python版〗
自动核对名单详细教程〖Python版〗
|
3天前
|
测试技术
Appium+python自动化(三十九)-Appium自动化测试框架综合实践 - 代码实现(超详解)
Appium+python自动化(三十九)-Appium自动化测试框架综合实践 - 代码实现(超详解)