Python测开28期-偕行-diff测试工具DeepDiff库

一、diff测试价值

  • 了解应用变更;
  • 对被测结果进行细粒度的断言;
  • 无须进行显式断言;
  • web app service 都可以应用:UI页面对象对比、接口数据对比;

image

二、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

image

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测试:

2、uiautomator dump使用

(1)通过adb命令获取界面的xml文件数据

  • 步骤:
  • 1、获取当前页面:adb shell uiautomator dump [file]
    • [file]:用于指定文件名,如果不指定会有默认文件名;
    • 文件保存路径为:
  • 2、从手机/sdcard拉取文件到本地路径:adb pull /sdcard/window_dump.xml C:\Users\Desktop
    image

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'))

结果: