每天读点官方文档之pytest (1)

注:内容不全和官方文档相同,只是按照官方文档顺序随心记录,与诸位做参考而已

开始

安装pytest

使用命令 pip install -U pytest 即可安装pytest 使用 pytest --version可以确认pytest安装是否成功以及安装的版本

整体测试环境如下:

(real) aaron@localhost:/raven/project/PycharmProjects/first> python -V
Python 3.9.4

(real) aaron@localhost:/raven/project/PycharmProjects/first> pip -V
pip 21.1.3 from /home/aaron/.virtualenvs/real/lib/python3.9/site-packages/pip (python 3.9)

(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest --version
pytest 6.2.5

创建第一个测试

  1. 新建一个叫做 test_sample.py 文件中包含一个方法和测试
"""
pytest 默认会查找当前目录以及其子目录中所有的test_*.py或者*_test.py格式的文件
并且运行其中test_*格式的方法
不符合格式要求的文件以及方法会被略过
"""


# 测试的方法,比如这里我们写了一些逻辑,获取各种信息并处理返回
# 这里只是简单逻辑,作为示例使用
def func(x):
    result = f"拿到{x},做一些运算....."
    return result


# pytest检测到这里就会执行这个函数
def test_func():
    # 正常业务处理
    x = "some value"
    value = func(x)

    # 打印当前
    print("=" * 20)
    print(value)
    print("=" * 20)
    # 断言。pytest直接使用断言判断用例失败
    assert x in value

  1. 运行测试文件。pytest运行还是十分简单的,只要terminal中进入目录,直接输入pytest即可。
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest
=============================================== test session starts ===============================================
platform linux -- Python 3.9.4, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
collected 1 item                                                                                                  

test_sample.py .                                                                                            [100%]

================================================ 1 passed in 0.10s ================================================
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest -s
=============================================== test session starts ===============================================
platform linux -- Python 3.9.4, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
collected 1 item                                                                                                  

test_sample.py ====================
拿到some value,做一些运算.....
====================
.

================================================ 1 passed in 0.10s ================================================
(real) aaron@localhost:/raven/project/PycharmProjects/first> 

注意上下两次执行结果的不同,pytest默认不打印print的内容,如果需要打印,需要添加-s参数

下面是正常执行的过程内容:

# 输入pytest执行目录下的所有符合规则的文件以及文件中的方法,这里只是说符合规则是因为匹配模式是可以修改的,具体如何修改会在之后有介绍
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest

# pytest开始执行用例
=============================================== test session starts ===============================================

# 打印基础信息
platform linux -- Python 3.9.4, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first

# pytest玩命采集到1个符合要求的用例
collected 1 item                                                                                                  

# 赶紧执行用例,用例在test_sample.py中,后面跟着几个点就是有几个用例执行成功
# 最后的100%是显示的执行完成的用例所占的百分比
test_sample.py .                                                                                            [100%]

# 用例执行完成,展示执行结果,这里显示为1个通过,并且在0.10s内执行完成
================================================ 1 passed in 0.10s ================================================
  1. 既然pytest使用断言判断测试用例是否执行成功,那么我们就可以自然得出一个推断,它应该对错误也有捕获。

修改测试用例新增test_fun2 test_fun3和test_fun4函数,部分内容为下面的内容:

def func(x):
    result = f"拿到{x},做一些运算....."
    return result


def test_func():
    ......
    assert x in value


def test_fun2():
    x = "2a"
    x = int(x)
    assert x == 2


def test_func3():
    assert 3 == 3


def test_func4():
    pass

执行结果为:

(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest
====================================================================== test session starts =======================================================================
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
collected 4 items                                                                                                                                                

test_sample.py .F..                                                                                                                                        [100%]

============================================================================ FAILURES ============================================================================
___________________________________________________________________________ test_fun2 ____________________________________________________________________________

    def test_fun2():
        x = "2a"
>       x = int(x)
E       ValueError: invalid literal for int() with base 10: '2a'

test_sample.py:31: ValueError
==================================================================== short test summary info =====================================================================
FAILED test_sample.py::test_fun2 - ValueError: invalid literal for int() with base 10: '2a'
================================================================== 1 failed, 3 passed in 0.02s ===================================================================

接下来来一起分析一下报错。

# 执行pytest
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest

# 开始执行测试用例
====================================================================== test session starts =======================================================================

# 展示基本信息
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first

# 收集到了4个测试用例
collected 4 items                                                                                                                                                

# 执行test_sample.py文件,第二个失败了,所以显示了F
# 所有4个用例都执行了,所以总体执行进度为100%
# 可以看到中间的用例执行失败,发生异常并不会影响后续用例的执行
# 同时判断条件比较宽泛,只要没有引起异常就判断为正常通过,不会管测试用例执行逻辑
test_sample.py .F..                                                                                                                                        [100%]

# 这次因为有执行错误的用例,所以会进行单独显示
============================================================================ FAILURES ============================================================================

# 执行错误用例的用例名称
___________________________________________________________________________ test_fun2 ____________________________________________________________________________

    def test_fun2():
        x = "2a"
        
# 用例执行错误的位置
>       x = int(x)
# 错误的原因
E       ValueError: invalid literal for int() with base 10: '2a'

# 对错误的其他信息展示
test_sample.py:31: ValueError
==================================================================== short test summary info =====================================================================
FAILED test_sample.py::test_fun2 - ValueError: invalid literal for int() with base 10: '2a'
================================================================== 1 failed, 3 passed in 0.02s ===================================================================

断言一个确定的异常

如果一个异常是我们已经确定的了,可以指定异常,从而能跳过异常判断

# 导入pytest库
import pytest


def fun(x):
    # 返回字典x的键a所对应的值
    return x["a"]


def test_func():
    # x定义为空字典
    x = {}
    # 通过使用pytest.raises包裹指定的异常,而对指定的异常不判定为失败
    with pytest.raises(KeyError):
        # 这里发生了KeyError的错误,但是因为使用pytest.raises包裹,并不判断为失败
        fun(x)


def test_func2():
    with pytest.raises(KeyError):
        # 这里会先发生异常,但是包裹的是KeyError而发生的是ValueError,所以会判定用例失败
        x = int("a")
        fun(x)


def test_func3():
    # 多个异常可以使用元祖类型进行传入,这里忽略两种异常,所以这个用例执行会成功
    with pytest.raises((KeyError, ValueError)):
        x = int("a")
        fun(x)


def test_func4():
    # 这里这个用例主要是要看看正常assert产生错误的时候如果有变量,pytest会把变量也进行打印,方便判断
    x = {"a": 2}
    assert fun(x) == 3

整体的输出如下,请结合代码中的解释自行分析输出结果

(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest
\====================================================================== test session starts =======================================================================
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
collected 4 items                                                                                                                                                

test_sample.py .F.F                                                                                                                                        [100%]

============================================================================ FAILURES ============================================================================
___________________________________________________________________________ test_func2 ___________________________________________________________________________

    def test_func2():
        with pytest.raises(KeyError):
>           x = int("a")
E           ValueError: invalid literal for int() with base 10: 'a'

test_sample.py:16: ValueError
___________________________________________________________________________ test_func4 ___________________________________________________________________________

    def test_func4():
        x = {"a": 2}
>       assert fun(x) == 3
E       AssertionError: assert 2 == 3
E        +  where 2 = fun({'a': 2})

test_sample.py:28: AssertionError
==================================================================== short test summary info =====================================================================
FAILED test_sample.py::test_func2 - ValueError: invalid literal for int() with base 10: 'a'
FAILED test_sample.py::test_func4 - AssertionError: assert 2 == 3
================================================================== 2 failed, 2 passed in 0.03s ===================================================================
(real) aaron@localhost:/raven/project/PycharmProjects/first> 

使用class编组测试

如果有一组测试用例可以进行分组,那么可以将测试用例放进class中,比如下面这样

# 编组的class 命名规则必须符合Test*的格式,注意,与函数、方法和文件不同,类命名的时候不强制要求带有下划线
# 同时,类中不能存在__init__方法的存在,否则也会被忽略不执行
class TestClass:
    # 与函数相同,如果想被测试,也需要test_开头
    def test_1(self):
        assert 1 == 1

    def test_2(self):
        assert 2 == 2

接下来我所有的用例执行都会使用pycharm进行手动执行,事例的格式有些不同,但是整体思路是相同的。

安装pytest后,pycharm会在每个可以执行的用例左边有一个绿色三角的运行标志。

使用那个标志运行即可,pycharm运行时添加了一些参数,我这里先说一下,之后我就只截取显示部分,其余部分诸君请自行尝试。

这里我点的是TestClass左边的绿色执行按钮。

# "/home/aaron/.virtualenvs/real/bin/python"这个是python执行文件
# "/raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py" pycharm 执行pytest的插件脚本
# "--target test_sample.py::TestClass" 执行目标,使用pytest也可以这样执行 比如pytest test_sample.py::TestClass 就只会执行TestClass测试用例,其他的测试用例都会被忽略
/home/aaron/.virtualenvs/real/bin/python /raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py --target test_sample.py::TestClass
# 测试开始执行时间
Testing started at 12:37 AM ...
# 警告信息,pycharm插件问题,可以忽略
/raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py:6: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives
  from distutils import version
# 配置pytest参数 --no-header 不展示头信息,也就是目录版本等信息 --no-summary 没有总结信息 -q 最简短模式输出,使用--quit效果相同
Launching pytest with arguments test_sample.py::TestClass --no-header --no-summary -q in /raven/project/PycharmProjects/first

# 开始真正的测试输出
============================= test session starts ==============================
# 采集到了两个测试用例
collecting ... collected 2 items

# 执行了哪个用例,执行结果如何,执行完成本用例后整体用例执行了多少
test_sample.py::TestClass::test_1 PASSED                                 [ 50%]
test_sample.py::TestClass::test_2 PASSED                                 [100%]

# 简短总结信息
============================== 2 passed in 0.01s ===============================

Process finished with exit code 0

现在我们将测试用例进行修改,再执行一下试试

class TestClass:
    def test_1(self):
        assert 1 == 1

    def test_2(self):
        # 修改这里的判断条件,必定会执行失败
        assert 3 == 2

输出如下,请诸君自行分析输出的信息

/home/aaron/.virtualenvs/real/bin/python /raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py --target test_sample.py::TestClass
Testing started at 1:02 AM ...
/raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py:6: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives
  from distutils import version
Launching pytest with arguments test_sample.py::TestClass --no-header --no-summary -q in /raven/project/PycharmProjects/first

============================= test session starts ==============================
collecting ... collected 2 items

test_sample.py::TestClass::test_1 
test_sample.py::TestClass::test_2 PASSED                                 [ 50%]FAILED                                 [100%]
test_sample.py:4 (TestClass.test_2)
3 != 2

Expected :2
Actual   :3
<Click to see difference>

self = <test_sample.TestClass object at 0x7f5fbded9bd0>

    def test_2(self):
        # 修改这里的判断条件,必定会执行失败
>       assert 3 == 2
E       assert 3 == 2

test_sample.py:7: AssertionError




========================= 1 failed, 1 passed in 0.03s ==========================

Process finished with exit code 1

需要注意的一点是,虽然将几个方法进行分组了,但是几个方法之间并不能共享修改的变量,这里使用官方的一个例子说明

class TestClass:
    value = 0

    def test_func1(self):
        self.value = self.value + 1
        assert self.value == 1

    def test_func2(self):
        assert self.value == 1

输出结果为:

============================= test session starts ==============================
collecting ... collected 2 items

test_sample.py::TestClass::test_func1 
test_sample.py::TestClass::test_func2 PASSED                             [ 50%]FAILED                             [100%]
test_sample.py:7 (TestClass.test_func2)
0 != 1

Expected :1
Actual   :0
<Click to see difference>

self = <test_sample.TestClass object at 0x7effad1420a0>

    def test_func2(self):
>       assert self.value == 1
E       assert 0 == 1

test_sample.py:9: AssertionError




========================= 1 failed, 1 passed in 0.03s ==========================

可以看到虽然test_sample.py::TestClass::test_func1先执行了,但是test_sample.py::TestClass::test_func2依旧失败了,self.value的值依旧是0 这是为什么呢?

给上面的代码增加一些东西

import json
import time


# 构造一个记录类,记录测试用例执行的时间以及在内存中的地址
class RecordClass:
    # 记录的列表
    record_list = []

    # 这里i是实例化后的
    def __init__(self, i=0):
        # 当前时间
        t = time.time()
        # 将时间和内存中的地址添加到list
        self.record_list.append((t, i))
        # 将列表存入文件,方便查看
        with open("record_list.txt", "a") as f:
            f.write(json.dumps(self.record_list))
            f.write("\n")


class TestClass:
    value = 0

    def test_func1(self):
        # 记录自身的内存地址
        RecordClass(id(self))
        self.value = self.value + 1
        assert self.value == 1

    def test_func2(self):
        RecordClass(id(self))
        assert self.value == 1

继续执行TestClass 然后查看record_list.txt内容如下:

# test_sample.py::TestClass::test_func1的类内存地址为139887927254032
[[1634199757.6280792, 139887927254032]]
# test_sample.py::TestClass::test_func2的类内存地址为139887927253600
[[1634199757.6280792, 139887927254032], [1634199757.6308126, 139887927253600]]

根据内存地址的不同,可以看到两个case执行了两个不同的类,也就是说是两个单独的事例,也就是func1修改的只是func1实例的self.value没有影响到func2的实例,所以导致func2的self.value还是默认的0,于是失败了。

至于如果在类间传递变量也是有方法的,还是需要使用pytest提供的方法。对于上面失败的原因,本人盲猜是因为pytest内部使用了多线程,每个测试用例一个线程去跑,当然具体原因还是有时间的时候看看源代码再了解吧。

注意:RecordClass中的record_list能够不断写入的原因是因为record_list是一个可变类型,如果TestClass的value属性也是可变类型也会不断存入数据。具体原因请各位自行查阅,这里不在赘述。

pytest内置的参数

pytest包含很多内置参数,提供给需求的开发者调用,这里使用官网的例子举例,具体的查看地址为 Builtin fixtures/function arguments

def test_tmp_dir(tmpdir):
    print(tmpdir)
    assert 0

其中tmpdir就是pytest提供的一个内置参数。整体函数执行的结果如下:

============================= test session starts ==============================
collecting ... collected 1 item

test_sample.py::test_tmp_dir FAILED                                      [100%]/tmp/pytest-of-aaron/pytest-3/test_tmp_dir0

test_sample.py:0 (test_tmp_dir)
tmpdir = local('/tmp/pytest-of-aaron/pytest-3/test_tmp_dir0')

    def test_tmp_dir(tmpdir):
        print(tmpdir)
>       assert 0
E       assert 0

test_sample.py:3: AssertionError


============================== 1 failed in 0.05s ===============================

可以看到已经把临时目录打印出来了。

如果想通过命令行确认含有哪些内置属性或者手动定制属性都可以通过pytest --fixtures进行查询

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容