一、diff测试价值
- 了解应用变更;
- 对被测结果进行细粒度的断言;
- 无须进行显式断言;
- web app service 都可以应用:UI页面对象对比、接口数据对比;
二、diff通用工具–DeepDiff
1、什么是DeepDiff
DeepDiff库常用来校验两个对象是否一致,并找出其中差异之处。
DeepDiff库由以下功能模块组成:
- DeepDiff: 该模块通过递归方式比较两个字典、可迭代对象、字符串和其他对象的深度差异. DeepDiff
- DeepSearch: 该模支持在对象中搜索对象. DeepSearch
- DeepHash: 对于2个对象的深度差异基于其内容对任何对象进行哈希,即使它们在Python眼中不可“哈希”. DeepHash
- Delta: 可应用于其他对象的对象增量。想象一下git提交,但对于结构化数据. Delta
- Extract: 该模块可以根据值抽取其Key的路径;反过来根据Key路径提取其值 Extract
- Commandline: 安装DeepDiff,你也可以在终端通过命令行本地的两个文件的异同。支持主流的文件格式如csv、tsv、 json、yaml等 Command Line
DeepDiff works with Python 3.4, 3.5, 3.6, 3.7, Pypy3
2、DeepDiff的使用
-
通过递归方式比较两个字典、可迭代对象、字符串和其他对象的深度差异, 如果对比结果不同,将会给出下面对应的返回:
- 1、type_changes:类型改变的key
- 2、values_changed:值发生变化的key
- 3、dictionary_item_added:字典key添加
- 4、dictionary_item_removed:字段key删除
-
安装:
pip install deepdiff
; -
使用:直接创建DeepDiff对象传入需要对比的两个对象和需要的默认参数即可,构造函数源码如下;
def __init__(self,
t1,# old
t2,# new
cache_purge_level=1,
cache_size=0,
cache_tuning_sample_size=0,
custom_operators=None,# 通过函数编写定制化的比较规则或前提
cutoff_distance_for_pairs=CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT,
cutoff_intersection_for_pairs=CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT,
encodings=None,
exclude_obj_callback=None,
exclude_obj_callback_strict=None,
exclude_paths=None,# 排除不需比较的路径
include_obj_callback=None,
include_obj_callback_strict=None,
include_paths=None,
exclude_regex_paths=None,
exclude_types=None,
get_deep_distance=False,# 两个对象之间的深度距离,它是一个介于0到1之间的浮点数。值越高,则结果中展示的差异深度越高。
group_by=None,# 可以在处理字典列表使用,将对象转化为按Group_by定义的值分组对比。
group_by_sort_key=None,
hasher=None,
hashes=None,
ignore_encoding_errors=False,# 忽略编码的差异
ignore_nan_inequality=False,
ignore_numeric_type_changes=False,# 忽略数字类型的差异
ignore_order=False,# 忽略对比的顺序
ignore_order_func=None,# 设置忽略指定情况的顺序
ignore_private_variables=True,
ignore_string_case=False,# 忽略字母大小写的差异
ignore_string_type_changes=False,# 忽略字符串类型的差异
ignore_type_in_groups=None,
ignore_type_subclasses=False,
iterable_compare_func=None,
zip_ordered_iterables=False,
log_frequency_in_sec=0,
math_epsilon=None,
max_diffs=None,
max_passes=10000000,
number_format_notation="f",
number_to_string_func=None,
progress_logger=logger.info,
report_repetition=False,
significant_digits=None,# 小数点后用于比较的位数。
truncate_datetime=None,# 时间的比较截止什么段位,段位包括:年月日时分
verbose_level=1,# 有新增或减少key时,如果想key和value一起返回,把verbose_level设为2即可
view=TEXT_VIEW,
_original_type=None,
_parameters=None,
_shared_parameters=None,
**kwargs):
super().__init__()
参数详解:
- verbose_level–有新增或减少key时,如果想key和value一起返回,把verbose_level设为2即可,默认为1只返回键
# 有新增或减少key时,如果想key和value一起返回,把verbose_level设为2即可,默认为1只返回键
t1 = {1:1, 2:2, 3:3}
t2 = {1:1, 3:3, 5:6, 6:6}
# {'dictionary_item_added': {'root[5]': 6, 'root[6]': 6}, 'dictionary_item_removed': {'root[2]': 2}}
print(DeepDiff(t1, t2, verbose_level=2))
# {'dictionary_item_added': [root[5], root[6]], 'dictionary_item_removed': [root[2]]}
print(DeepDiff(t1, t2))
- group_by–可以在处理字典列表使用,将对象转化为按Group_by定义的值分组对比。
t1 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB', 'name': 'James', 'last_name': 'Blue'}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},]
t2 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB', 'name': 'James', 'last_name': 'Brown'}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},]
# {'values_changed': {"root[1]['last_name']": {'new_value': 'Brown', 'old_value': 'Blue'}}}
print(DeepDiff(t1, t2))
# {'values_changed': {"root['BB']['last_name']": {'new_value': 'Brown', 'old_value': 'Blue'}}}
print(DeepDiff(t1, t2, group_by='id'))
- custom_operators–通过函数编写定制化的比较规则或前提:(更多使用方法可参考官方文档)
from deepdiff import DeepDiff
from deepdiff.operator import BaseOperator
# 定义数据类
class CustomClass:
def __init__(self, d: dict, l: list):
self.dict = d
self.dict['list'] = l
# 初始化数据
custom1 = CustomClass(d=dict(a=1, b=2), l=[1, 2, 3])
custom2 = CustomClass(d=dict(c=3, d=4), l=[1, 2, 3, 2])
custom3 = CustomClass(d=dict(a=1, b=2), l=[1, 2, 3, 4])
# 定义diff的比较规则
class ListMatchOperator(BaseOperator):
# 重新BaseOperator里面的方法--放弃比较
def give_up_diffing(self, level, diff_instance):
# 只有对象的dict中的dict值(set集合去重后的值)不相等的话才比较
if set(level.t1.dict['list']) == set(level.t2.dict['list']):
return True
print(DeepDiff(custom1, custom2, custom_operators=[ListMatchOperator(types=[CustomClass])]))#
# {'dictionary_item_added': [root.dict['a'], root.dict['b']], 'dictionary_item_removed': [root.dict['c'], root.dict['d']], 'values_changed': {"root.dict['list'][3]": {'new_value': 4, 'old_value': 2}}}
print(DeepDiff(custom2, custom3, custom_operators=[ListMatchOperator(types=[CustomClass])]))
- get_deep_distance–两个对象之间的深度距离,它是一个介于0到1之间的浮点数。值越高,则结果中展示的差异深度越高。
# {'values_changed': {'root': {'new_value': 10.1, 'old_value': 10.0}}, 'deep_distance': 0.0014925373134328302}
print(DeepDiff(10.0, 10.1, get_deep_distance=True))
- exclude_paths – 排除不需比较的路径,可以使用键的方式给路径,也可以使用正则指定路径;
t1 = {"for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"],"name":"python"}
t2 = {"for life": "vegan", "ingredients": ["veggies", "tofu", "soy sauce"],"name":"java"}
# 指定ingredients这个路径不对比差异
# {'values_changed': {"root['name']": {'new_value': 'java', 'old_value': 'python'}}}
print(DeepDiff(t1, t2, exclude_paths="root['ingredients']"))
# 也可指定多个路径,ingredients和name都不比较
print(DeepDiff(t1, t2, exclude_paths=["root['ingredients']","root['name']"]))# {}
import re
t1 = [{'a': 1, 'b': 2}, {'c': 4, 'b': 5}]
t2 = [{'a': 1, 'b': 3}, {'c': 4, 'b': 5}]
print(DeepDiff(t1, t2, exclude_regex_paths=r"root\[\d+\]\['b'\]"))
# 另一种写法
exclude_path = re.compile(r"root\[\d+\]\['b'\]")
print(DeepDiff(t1, t2, exclude_regex_paths=[exclude_path]))
- ignore_order–忽略对比的顺序
t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3]}}
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2, 3]}}
# 默认的ignore_order=False
print(DeepDiff(t1, t2))# {'iterable_item_added': {"root[4]['b'][1]": 3}}
print(DeepDiff(t1, t2, ignore_order=True))# {}--忽略了顺序,所以"b": [1, 2, 3]和"b": [1, 3, 2, 3]对比都能找到,没有差异
- ignore_order_func–忽略指定情况的顺序,需要给一个方法名;
t1 = {'a': [1, 2], 'b': [3, 4]}
t2 = {'a': [2, 1], 'b': [4, 3]}
# 设置只忽略a路径的顺序
def ignore_order_func(level):
return 'a' in level.path()
# 忽略a的顺序,b的不忽略
# {'values_changed': {"root['b'][0]": {'new_value': 4, 'old_value': 3}, "root['b'][1]": {'new_value': 3, 'old_value': 4}}}
print(DeepDiff(t1, t2, ignore_order_func=ignore_order_func))
print(DeepDiff(t1, t2, ignore_order=True))# {} --忽略顺序,只要键值都有就行
-
ignore_encoding_errors=False:忽略编码的差异
-
ignore_string_type_changes=True : 忽略字符串类型的差异
-
ignore_numeric_type_changes=True:忽略数字类型的差异
-
ignore_string_case=True:忽略字母大小写的差异
-
truncate_datetime=‘minute’:时间的比较截止到分钟,只比较年月日小时
-
significant_digits–小数点后用于比较的位数。
from decimal import Decimal
t1 = Decimal('1.52')
t2 = Decimal('1.54')
t3 = Decimal('1.53')
t4 = Decimal('1.55')
# 小数点后0位比较(近似比较)--相差00.3
print(DeepDiff(t1, t4, significant_digits=0))# {}
# 小数点后1位比较(近似比较)--相差0.02
print(DeepDiff(t1, t2, significant_digits=1))# {}
# 相差00.3--{'values_changed': {'root': {'new_value': Decimal('1.55'), 'old_value': Decimal('1.52')}}}
print(DeepDiff(t1, t4, significant_digits=1))
# 小数点后2位比较--相差0.01--{'values_changed': {'root': {'new_value': Decimal('1.53'), 'old_value': Decimal('1.54')}}}
print(DeepDiff(t2, t3, significant_digits=2))
3、 一些本身的设置
Serialization 序列化
- ddiff.to_dict(view_override=‘text’):转化为dict;
- ddiff.to_json():转化为json;
View视图
- view=’tree’:设置视图为树,默认为text view;
- DeepDiff(t1, t2).pretty():输出可读化结果;
Stats and Logging
- DeepDiff(t1, t2, log_frequency_in_sec=1):按1s的频率输出操作日志;
- diff.get_stats():在diff对象上运行get_stats()方法可以获取该对象的一些统计信息;
三、diff在测试工作中的实际运用
1、比较对象范畴及结果结构
- UI界面diff测试:
- uiautomator dump–获取页面的xml文件;
- appium page sourece–TODO;
- 截图对比;–TODO;
- 接口diff测试:
- 获取响应结果进行对比JSON schema;
2、uiautomator dump使用
(1)通过adb命令获取界面的xml文件数据
- 步骤:
- 1、获取当前页面:
adb shell uiautomator dump [file]
;- [file]:用于指定文件名,如果不指定会有默认文件名;
- 文件保存路径为:
- 2、从手机/sdcard拉取文件到本地路径:
adb pull /sdcard/window_dump.xml C:\Users\Desktop
;
2、使用DeepDiff对比两个xml文件
- 1、将xml文件直接转成JSON文件通过DeepDiff对比,但是由于数据量很大,脚本执行效率很低;
- 2、自己封装一个方法,只获取xml文件中需要被比较的字段存入的dict中再进行对比;
通过uiautomator dump获取并做修改的xml源文件:
xmldiff.py
from deepdiff import DeepDiff
import xml.etree.ElementTree as ET
class XMLDiff:
# 使用DeepDiff封装自定义的diff方法
def diff(self, old, new):
tree = ET.parse(old)
old = tree.getroot()
old_object = self.xml2dict(old)
print(f"old_object==========================================={old_object}")
tree = ET.parse(new)
new = tree.getroot()
new_object = self.xml2dict(new)
print(f"new_object==========================================={new_object}")
# 通过DeepDiff进行比较
ddiff = DeepDiff(old_object, new_object)
# 把比较结果转成dict
return ddiff.to_dict()
# 把xml转成dict
def xml2dict(self, root: ET.Element):
# 只获取需要检查的字段('resource-id', 'content-desc', 'text', 'class','bounds')放入的dict中
res = {k: v for k, v in root.items() if k in ['resource-id', 'content-desc', 'text', 'class','bounds']}
# dict新增一个键children,值的类型为列表
res['children'] = []
# 遍历文件,通过递归获取所有需要的字段值
for child in root:
res['children'].append(self.xml2dict(child))
return res
if __name__ == '__main__':
# print(yaml.dump(XMLDiff().diff('../data/old.xml', '../data/new.xml')))
print(XMLDiff().diff('../data/old.xml', '../data/new.xml'))
结果: