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字符串处理实现单词反转
Jun 14 Python
Python 处理数据的实例详解
Aug 10 Python
Python实现将HTML转换成doc格式文件的方法示例
Nov 20 Python
利用python的socket发送http(s)请求方法示例
May 07 Python
使用Django2快速开发Web项目的详细步骤
Jan 06 Python
Python Datetime模块和Calendar模块用法实例分析
Apr 15 Python
Python对接支付宝支付自实现功能
Oct 10 Python
pytorch1.0中torch.nn.Conv2d用法详解
Jan 10 Python
opencv python图像梯度实例详解
Feb 04 Python
PyCharm无法引用自身项目解决方式
Feb 12 Python
详解appium自动化测试工具(monitor、uiautomatorviewer)
Jan 27 Python
Python软件包安装的三种常见方法
Jul 07 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 生成饼图 三维饼图
2009/09/28 PHP
php利用header函数实现文件下载时直接提示保存
2009/11/12 PHP
php获取域名的google收录示例
2014/03/24 PHP
PHP命名空间(Namespace)简明教程
2014/06/11 PHP
php基于CodeIgniter实现图片上传、剪切功能
2016/05/14 PHP
Laravel框架使用Seeder实现自动填充数据功能
2018/06/13 PHP
javascript 全角转换实现代码
2009/07/17 Javascript
JavaScript中使用ActiveXObject操作本地文件夹的方法
2014/03/28 Javascript
JS实现的论坛Ajax打分效果完整实例
2015/10/31 Javascript
jQuery 获取遍历获取table中每一个tr中的第一个td的方法
2016/10/05 Javascript
H5实现中奖记录逐行滚动切换效果
2017/03/13 Javascript
JS中IP地址与整数相互转换的实现代码
2017/04/10 Javascript
JS表单提交验证、input(type=number) 去三角 刷新验证码
2017/06/21 Javascript
es6 字符串String的扩展(实例讲解)
2017/08/03 Javascript
vue el-table实现自定义表头
2019/12/11 Javascript
JavaScript中变量提升机制示例详解
2019/12/27 Javascript
python获得文件创建时间和修改时间的方法
2015/06/30 Python
numpy找出array中的最大值,最小值实例
2018/04/03 Python
在Pycharm中修改文件默认打开方式的方法
2019/01/17 Python
python科学计算之scipy——optimize用法
2019/11/25 Python
python内打印变量之%和f的实例
2020/02/19 Python
python pandas利用fillna方法实现部分自动填充功能
2020/03/16 Python
解决Django no such table: django_session的问题
2020/04/07 Python
Python enumerate() 函数如何实现索引功能
2020/06/29 Python
python热力图实现简单方法
2021/01/29 Python
分享8款纯CSS3实现的搜索框功能
2017/09/14 HTML / CSS
化石印度尼西亚在线商店:Fossil Indonesia
2019/03/11 全球购物
英国绿色商店:Natural Collection
2019/05/03 全球购物
MIKI HOUSE美国官方网上商店:日本领先的婴儿和儿童高级时装品牌
2020/06/21 全球购物
技术人员面试提纲
2013/11/28 职场文书
个人授权委托书
2014/04/03 职场文书
访谈节目策划方案
2014/05/15 职场文书
党的群众路线教育实践活动领导班子整改措施
2014/10/28 职场文书
毕业论文致谢词
2015/05/14 职场文书
导游词之重庆渣滓洞
2020/01/08 职场文书
python 自动刷新网页的两种方法
2021/04/20 Python