Assert 可以说得上是 Pytest 的灵魂。比起其他测试框架,Pytest 的断言机制非常简洁,它屏蔽了表达式的类型、断言的类型,只需使用 assert 关键字( 而不是 AssertionTrue 之类函数) 即可完成测试用例的检查。
本文内容
Assert 相关代码在 _pytest/assertion/ 文件中,其中 util.py 是断言的作用代码。本文分析的是这里面的代码。
在 Pytest 中,断言的格式一般是这样:assert exp1 op exp2,util.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等转义符的字符串; - 然后将这个字符串传入外部库
difflib的ndiff函数得到结果。
_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
3strA = '2'
strB = '2345'
diff=['- 345', '+ 2345', '? +'] - 经过函数中的格式处理,输出变成:
1
2E 2345
E ? ++标识了strA 的出现位置