pytest源码分析-assert机制

Assert 可以说得上是 Pytest 的灵魂。比起其他测试框架,Pytest 的断言机制非常简洁,它屏蔽了表达式的类型、断言的类型,只需使用 assert 关键字( 而不是 AssertionTrue 之类函数) 即可完成测试用例的检查。

本文内容

Assert 相关代码在 _pytest/assertion/ 文件中,其中 util.py 是断言的作用代码。本文分析的是这里面的代码。

在 Pytest 中,断言的格式一般是这样:assert exp1 op exp2util.py 中的代码分为三类:

  • format代码:规范 exp1 和 exp2 格式;
  • 类型判断代码:判断 exp1 和 exp2 类型是否一致;
  • 内容比较:比较 exp1 和 exp2 区别,返回结果。

由于 format 代码几乎都是 pprint 的封装,所以姑且先不分析了。

util.py入口

断言被传入 util.py 后,首先进入 assertrepr_compare(...) ,这个函数是util 的骨架,它上一节中所有类型代码检查表达式。

进入assert_compare 后,首先,函数检查命令行参数携带几个 v 。命令行调用 pytest 的格式是 pytest [-v] [filepath] ,可以输入多个 v,v越多输出越详细。

在这里,如果 v 数量多于两个,函数将用 pprint.pformat 规范表达式的格式,比如把 list 变为每个元素换一行的格式,美化后面内容比较的输出。

断言中 op 可能是 ==not in ,根据op进入不同分支后,执行类型判断函数判。

类型判断函数

类型判断函数用于判断op两边的表达式是否为同一数据类型。有如下类型判断函数

  • issequence
  • isset
  • istext
  • isdict
  • isattrs
  • isdatacls
  • isiterable

其中 sequence set dict text 类型直接通过 isinstance() 函数判断并返回布尔类型;
isdatacls 返回是否有 __dataclass _fileds__ 字段;isattrs 返回是否有 __attrs_attrs__ 字段。这两个的结果会被元组包含,传入后文所提的 _compare_eq_cls() 函数中
isiterable 用传入对象生成迭代器,返回是否能生成迭代器。

内容比较

内容比较函数在确定两边表达式类型相同后,比较两边表达式内的具体数据内容。
op 为断言的 operation

先看 op 为 == 时的比较:

diff_text() 是字符串的比较,它通过外部库 difflib 库实现功能。
他有不同详细程度的输出:

  • 当参数v数量小于 2 :从头到尾从尾到头比较两个字符串,如果相同字符的数量大于 42,就把相同字符的部分截掉,只留下不同的部分;
  • 当参数 v 数量大于等于 2 :保留所有的字符串内容。
  • 把两个字符串按 \n \r\n 分割成两个列表,将两个列表传入 difflib.ndiff() 函数
  • ndiff() 函数将返回两个列表不同的地方。

_compare_eq_set() 是集合比较,它通过集合特性实现功能。

  • 左边的集合 - 右边的集合;右边的集合-左边的集合;
  • 得到的就是两边不一样的内容

_compare_eq_dict() 是字典比较。

  • 集合形式返回两个字典的键,得出共有的键;
  • 如果参数 v 数量小于 2 ,不处理字典键,否则按 \n \r\n 分割字典键;
  • 比较相同键的值,值不同则放入diff 列表;
  • 像处理集合一样处理字典键;
  • 最后返回不同的值和不同的键;

上面的数据的共通点是,他们都是可迭代的。Pytest 专门对迭代器还有特定的比较函数,当命令行参数大于等于两个 v 时,上面的数据结构就会进入迭代器比较。

_compare_eq_iterable() 是比较可迭代数据的函数。

  • 传入左右表达式之后,用\n \r\n 分割成列表
  • 如果得到的列表长度不一样,将其传入 AlwaysDispatchingPrettyPrinter 类,这个类会把列表变成一个保留 , [ ] 并转义 \n 等转义符的字符串;
  • 然后将这个字符串传入外部库 difflibndiff 函数得到结果。

_compare_eq_cls() 是类比较。

  • 传入两个类,如果这两个类被 @dataclass@attrs 装饰,将类中属性用__dataclass_fields____attrs_attrs__ 把第一个类的类属性拿出来,放进field_to_check 列表;
  • 遍历 field_to_check ,从两个类中取出类属性,如果类属性值相等,放进 same 列表,否则放进 diff 列表
  • 输出具体哪些属性不同,如果命令行 v 数量大于等于 2,还要输出有哪些属性时相同的。

然后再看看 op 为 not in 的情况

not in 只有一个情况,就是检查左边的字符串是否在右边字符串中。其实 Pytest 也可以检查列表A 是否在列表B 中,字典A是否在字典B 中…等情况,但代码不在这个util.py中。

_notin_text 用于检查左边的字符串是否在右边的字符串中。

  • 首先直接使用字符串自带函数 find(strA) 找到字符串A在字符串B中的位置,然后令 correct 等于字符串B 除去字符串A剩下的字符串
  • 将 correct 放进上文的 _diff_text 得到字符串不同的地方
  • 假设 strA/strB 如下, 则 diff 为:
    1
    2
    3
    strA = '2' 
    strB = '2345'
    diff=['- 345', '+ 2345', '? +']
  • 经过函数中的格式处理,输出变成:
    1
    2
    E           2345
    E ? +
    + 标识了strA 的出现位置