pytest中文文档之编写断言


Posted in Python onSeptember 12, 2019

编写断言

使用assert编写断言

pytest允许你使用python标准的assert表达式写断言;

例如,你可以这样做:

# test_sample.py

def func(x):
 return x + 1


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

如果这个断言失败,你会看到func(3)实际的返回值:

/d/Personal Files/Python/pytest-chinese-doc/src (5.1.2)
λ pytest test_sample.py
================================================= test session starts =================================================
platform win32 -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: D:\Personal Files\Python\pytest-chinese-doc\src, inifile: pytest.ini
collected 1 item

test_sample.py F                         [100%]

====================================================== FAILURES ======================================================= _____________________________________________________ test_sample _____________________________________________________

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

test_sample.py:28: AssertionError
================================================== 1 failed in 0.05s ==================================================

pytest支持显示常见的python子表达式的值,包括:调用、属性、比较、二进制和一元运算符等(参考pytest支持的python失败时报告的演示);

这允许你在没有模版代码参考的情况下,可以使用的python的数据结构,而无须担心丢失自省的问题;

同时,你也可以为断言指定了一条说明信息,用于失败时的情况说明:

assert a % 2 == 0, "value was odd, should be even"

编写触发期望异常的断言

你可以使用pytest.raises()作为上下文管理器,来编写一个触发期望异常的断言:

import pytest


def myfunc():
 raise ValueError("Exception 123 raised")


def test_match():
 with pytest.raises(ValueError):
  myfunc()

当用例没有返回ValueError或者没有异常返回时,断言判断失败;

如果你希望同时访问异常的属性,可以这样:

import pytest


def myfunc():
 raise ValueError("Exception 123 raised")


def test_match():
 with pytest.raises(ValueError) as excinfo:
  myfunc()
 assert '123' in str(excinfo.value)

其中,excinfo是ExceptionInfo的一个实例,它封装了异常的信息;常用的属性包括:.type、.value和.traceback;

注意:在上下文管理器的作用域中,raises代码必须是最后一行,否则,其后面的代码将不会执行;所以,如果上述例子改成:

def test_match():
 with pytest.raises(ValueError) as excinfo:
  myfunc()
  assert '456' in str(excinfo.value)

则测试将永远成功,因为assert '456' in str(excinfo.value)并不会执行;

你也可以给pytest.raises()传递一个关键字参数match,来测试异常的字符串表示str(excinfo.value)是否符合给定的正则表达式(和unittest中的TestCase.assertRaisesRegexp方法类似):

import pytest


def myfunc():
 raise ValueError("Exception 123 raised")


def test_match():
 with pytest.raises((ValueError, RuntimeError), match=r'.* 123 .*'):
  myfunc()

pytest实际调用的是re.search()方法来做上述检查;并且,pytest.raises()也支持检查多个期望异常(以元组的形式传递参数),我们只需要触发其中任意一个;

pytest.raises还有另外的一种使用形式:

首先,我们来看一下它在源码中的定义:

# _pytest/python_api.py

def raises( # noqa: F811
 expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
 *args: Any,
 match: Optional[Union[str, "Pattern"]] = None,
 **kwargs: Any
) -> Union["RaisesContext[_E]", Optional[_pytest._code.ExceptionInfo[_E]]]:

它接收一个位置参数expected_exception,一组可变参数args,一个关键字参数match和一组关键字参数kwargs;

接着往下看:

# _pytest/python_api.py

 if not args:
  if kwargs:
   msg = "Unexpected keyword arguments passed to pytest.raises: "
   msg += ", ".join(sorted(kwargs))
   msg += "\nUse context-manager form instead?"
   raise TypeError(msg)
  return RaisesContext(expected_exception, message, match)
 else:
  func = args[0]
  if not callable(func):
   raise TypeError(
    "{!r} object (type: {}) must be callable".format(func, type(func))
   )
  try:
   func(*args[1:], **kwargs)
  except expected_exception as e:
   # We just caught the exception - there is a traceback.
   assert e.__traceback__ is not None
   return _pytest._code.ExceptionInfo.from_exc_info(
    (type(e), e, e.__traceback__)
   )
 fail(message)

其中,args如果存在,那么它的第一个参数必须是一个可调用的对象,否则会报TypeError异常;同时,它会把剩余的args参数和所有kwargs参数传递给这个可调用对象,然后检查这个对象执行之后是否触发指定异常;

所以我们有了一种新的写法:

pytest.raises(ZeroDivisionError, lambda x: 1/x, 0)

# 或者

pytest.raises(ZeroDivisionError, lambda x: 1/x, x=0)

这个时候如果你再传递match参数,是不生效的,因为它只有在if not args:的时候生效;

另外,pytest.mark.xfail()也可以接收一个raises参数,来判断用例是否因为一个具体的异常而导致失败:

@pytest.mark.xfail(raises=IndexError)
def test_f():
 f()

如果f()触发一个IndexError异常,则用例标记为xfailed;如果没有,则正常执行f();

注意:如果f()测试成功,用例的结果是xpassed,而不是passed;

pytest.raises适用于检查由代码故意引发的异常;而@pytest.mark.xfail()更适合用于记录一些未修复的Bug;

特殊数据结构比较时的优化

# test_special_compare.py

def test_set_comparison():
 set1 = set('1308')
 set2 = set('8035')
 assert set1 == set2


def test_long_str_comparison():
 str1 = 'show me codes'
 str2 = 'show me money'
 assert str1 == str2


def test_dict_comparison():
 dict1 = {
  'x': 1,
  'y': 2,
 }
 dict2 = {
  'x': 1,
  'y': 1,
 }
 assert dict1 == dict2

上面,我们检查了三种数据结构的比较:集合、字符串和字典;

/d/Personal Files/Python/pytest-chinese-doc/src (5.1.2)
λ pytest test_special_compare.py
================================================= test session starts =================================================
platform win32 -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: D:\Personal Files\Python\pytest-chinese-doc\src, inifile: pytest.ini
collected 3 items

test_special_compare.py FFF                      [100%]

====================================================== FAILURES ======================================================= _________________________________________________ test_set_comparison _________________________________________________

 def test_set_comparison():
  set1 = set('1308')
  set2 = set('8035')
>  assert set1 == set2
E  AssertionError: assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'}
E   Extra items in the left set:
E   '1'
E   Extra items in the right set:
E   '5'
E   Use -v to get the full diff

test_special_compare.py:26: AssertionError
______________________________________________ test_long_str_comparison _______________________________________________

 def test_long_str_comparison():
  str1 = 'show me codes'
  str2 = 'show me money'
>  assert str1 == str2
E  AssertionError: assert 'show me codes' == 'show me money'
E   - show me codes
E   ?   ^ ^ ^
E   + show me money
E   ?   ^ ^ ^

test_special_compare.py:32: AssertionError
________________________________________________ test_dict_comparison _________________________________________________

 def test_dict_comparison():
  dict1 = {
   'x': 1,
   'y': 2,
  }
  dict2 = {
   'x': 1,
   'y': 1,
  }
>  assert dict1 == dict2
E  AssertionError: assert {'x': 1, 'y': 2} == {'x': 1, 'y': 1}
E   Omitting 1 identical items, use -vv to show
E   Differing items:
E   {'y': 2} != {'y': 1}
E   Use -v to get the full diff

test_special_compare.py:44: AssertionError
================================================== 3 failed in 0.09s ==================================================

针对一些特殊的数据结构间的比较,pytest对结果的显示做了一些优化:

  • 集合、列表等:标记出第一个不同的元素;
  • 字符串:标记出不同的部分;
  • 字典:标记出不同的条目;

更多例子参考pytest支持的python失败时报告的演示

为失败断言添加自定义的说明

# test_foo_compare.py

class Foo:
 def __init__(self, val):
  self.val = val

 def __eq__(self, other):
  return self.val == other.val
 
 
def test_foo_compare():
 f1 = Foo(1)
 f2 = Foo(2)
 assert f1 == f2

我们定义了一个Foo对象,也复写了它的__eq__()方法,但当我们执行这个用例时:

/d/Personal Files/Python/pytest-chinese-doc/src (5.1.2)
λ pytest test_foo_compare.py
================================================= test session starts =================================================
platform win32 -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: D:\Personal Files\Python\pytest-chinese-doc\src, inifile: pytest.ini
collected 1 item

test_foo_compare.py F                       [100%]

====================================================== FAILURES ======================================================= __________________________________________________ test_foo_compare ___________________________________________________

 def test_foo_compare():
  f1 = Foo(1)
  f2 = Foo(2)
>  assert f1 == f2
E  assert <src.test_foo_compare.Foo object at 0x0000020E90C4E978> == <src.test_foo_compare.Foo object at 0x0000020E90C4E630>

test_foo_compare.py:37: AssertionError
================================================== 1 failed in 0.04s ==================================================

并不能直观的看出来失败的原因;

在这种情况下,我们有两种方法来解决:

  • 复写Foo的__repr__()方法:
def __repr__(self):
  return str(self.val)

我们再执行用例:

luyao@NJ-LUYAO-T460 /d/Personal Files/Python/pytest-chinese-doc/src (5.1.2)
λ pytest test_foo_compare.py
================================================= test session starts =================================================
platform win32 -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: D:\Personal Files\Python\pytest-chinese-doc\src, inifile: pytest.ini
collected 1 item

test_foo_compare.py F                                              [100%]

====================================================== FAILURES ======================================================= __________________________________________________ test_foo_compare ___________________________________________________

  def test_foo_compare():
    f1 = Foo(1)
    f2 = Foo(2)
>    assert f1 == f2
E    assert 1 == 2

test_foo_compare.py:37: AssertionError
================================================== 1 failed in 0.06s ==================================================

这时,我们能看到失败的原因是因为1 == 2不成立;

至于__str__()和__repr__()的区别,可以参考StackFlow上的这个问题中的回答:https://stackoverflow.com/questions/1436703/difference-between-str-and-repr

  • 使用pytest_assertrepr_compare这个钩子方法添加自定义的失败说明
# conftest.py

from .test_foo_compare import Foo


def pytest_assertrepr_compare(op, left, right):
  if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
    return [
      "比较两个Foo实例:", # 顶头写概要
      "  值: {} != {}".format(left.val, right.val), # 除了第一个行,其余都可以缩进
    ]

再次执行:

/d/Personal Files/Python/pytest-chinese-doc/src (5.1.2)
λ pytest test_foo_compare.py
================================================= test session starts =================================================
platform win32 -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: D:\Personal Files\Python\pytest-chinese-doc\src, inifile: pytest.ini
collected 1 item

test_foo_compare.py F                                              [100%]

====================================================== FAILURES ======================================================= __________________________________________________ test_foo_compare ___________________________________________________

  def test_foo_compare():
    f1 = Foo(1)
    f2 = Foo(2)
>    assert f1 == f2
E    assert 比较两个Foo实例:
E      值: 1 != 2

test_foo_compare.py:37: AssertionError
================================================== 1 failed in 0.05s ==================================================

我们会看到一个更友好的失败说明;

关于断言自省的细节

当断言失败时,pytest为我们提供了非常人性化的失败说明,中间往往夹杂着相应变量的自省信息,这个我们称为断言的自省;

那么,pytest是如何做到这样的:

  • pytest发现测试模块,并引入他们,与此同时,pytest会复写断言语句,添加自省信息;但是,不是测试模块的断言语句并不会被复写;

复写缓存文件

pytest会把被复写的模块存储到本地作为缓存使用,你可以通过在测试用例的根文件夹中的conftest.py里添加如下配置:

import sys

sys.dont_write_bytecode = True

来禁止这种行为;

但是,它并不会妨碍你享受断言自省的好处,只是不会在本地存储.pyc文件了。

去使能断言自省

你可以通过一下两种方法:

  • 在需要去使能模块的docstring中添加PYTEST_DONT_REWRITE字符串;
  • 执行pytest时,添加--assert=plain选项;

我们来看一下去使能后的效果:

/d/Personal Files/Python/pytest-chinese-doc/src (5.1.2)
λ pytest test_foo_compare.py --assert=plain
================================================= test session starts =================================================
platform win32 -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: D:\Personal Files\Python\pytest-chinese-doc\src, inifile: pytest.ini
collected 1 item

test_foo_compare.py F                                              [100%]

====================================================== FAILURES ======================================================= __________________________________________________ test_foo_compare ___________________________________________________

  def test_foo_compare():
    f1 = Foo(1)
    f2 = Foo(2)
>    assert f1 == f2
E    AssertionError

test_foo_compare.py:37: AssertionError
================================================== 1 failed in 0.05s ==================================================

断言失败时的信息就非常的不完整了,我们几乎看不出任何有用的Debug信息;

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
Python的subprocess模块总结
Nov 07 Python
Windows上配置Emacs来开发Python及用Python扩展Emacs
Nov 20 Python
Python标准库笔记struct模块的使用
Feb 22 Python
Python实现的凯撒密码算法示例
Apr 12 Python
Python处理菜单消息操作示例【基于win32ui模块】
May 09 Python
Python json模块dumps、loads操作示例
Sep 06 Python
python 输出所有大小写字母的方法
Jan 02 Python
pandas实现excel中的数据透视表和Vlookup函数功能代码
Feb 14 Python
python进行参数传递的方法
May 12 Python
详解python对象之间的交互
Sep 29 Python
Python接口自动化测试框架运行原理及流程
Nov 30 Python
Python中异常处理用法
Nov 27 Python
python中调试或排错的五种方法示例
Sep 12 #Python
详解Python 中sys.stdin.readline()的用法
Sep 12 #Python
Python3将数据保存为txt文件的方法
Sep 12 #Python
Python3 tkinter 实现文件读取及保存功能
Sep 12 #Python
调试Django时打印SQL语句的日志代码实例
Sep 12 #Python
Python socket非阻塞模块应用示例
Sep 12 #Python
Python的条件锁与事件共享详解
Sep 12 #Python
You might like
php学习笔记之 函数声明(二)
2011/06/09 PHP
用PHP实现的四则运算表达式计算实现代码
2011/08/02 PHP
php生成txt文件标题及内容的方法
2014/01/16 PHP
通用于ie和firefox的函数 GetCurrentStyle (obj, prop)
2006/12/27 Javascript
用JavaScript实现UrlEncode和UrlDecode的脚本代码
2008/07/23 Javascript
关于Javascript模块化和命名空间管理的问题说明
2010/12/06 Javascript
JavaScript NaN和Infinity特殊值 [译]
2012/09/20 Javascript
js的touch事件的实际引用
2014/10/13 Javascript
jQuery修改li下的样式以及li下的img的src的值的方法
2014/11/02 Javascript
使用jquery 简单实现下拉菜单
2015/01/14 Javascript
AngularJS学习笔记之基本指令(init、repeat)
2015/06/16 Javascript
浅析Node.js 中 Stream API 的使用
2015/10/23 Javascript
JS+CSS实现的漂亮渐变背景特效代码(6个渐变效果)
2016/03/25 Javascript
jQuery实现图片轮播效果代码(基于jquery.pack.js插件)
2016/06/02 Javascript
React简单介绍
2017/05/24 Javascript
Angular.JS中select下拉框设置value的方法
2017/06/20 Javascript
JavaScript原型继承_动力节点Java学院整理
2017/06/30 Javascript
vuex实现登录状态的存储,未登录状态不允许浏览的方法
2018/03/09 Javascript
angularjs http与后台交互的实现示例
2018/12/21 Javascript
[15:28]DOTA2 HEROS教学视频教你分分钟做大人-剧毒术士
2014/06/13 DOTA
[49:15]DOTA2-DPC中国联赛 正赛 CDEC vs XG BO3 第二场 1月19日
2021/03/11 DOTA
[01:07:19]DOTA2-DPC中国联赛 正赛 CDEC vs XG BO3 第一场 1月19日
2021/03/11 DOTA
Python实现冒泡,插入,选择排序简单实例
2014/08/18 Python
Django1.7+python 2.78+pycharm配置mysql数据库教程
2014/11/18 Python
Cpy和Python的效率对比
2015/03/20 Python
基于Numpy.convolve使用Python实现滑动平均滤波的思路详解
2019/05/16 Python
Matplotlib 折线图plot()所有用法详解
2020/07/28 Python
python爬虫实现爬取同一个网站的多页数据的实例讲解
2021/01/18 Python
使用jTopo给Html5 Canva中绘制的元素添加鼠标事件
2014/05/15 HTML / CSS
美国女鞋品牌:naturalizer(娜然)
2016/08/01 全球购物
大学生怎样进行自我评价
2013/12/07 职场文书
高中家长寄语
2014/04/02 职场文书
小学教师培训方案
2014/06/09 职场文书
想要创业,那么你做好准备了吗?
2019/07/01 职场文书
2019奶茶店创业计划书范本,值得你借鉴
2019/08/14 职场文书
少年的你:世界上没有如果,要在第一次就勇敢的反抗
2019/11/20 职场文书