Skip to content

基于图片的可视化测试技术在自动化测试中的应用

基于图片的可视化测试技术在自动化测试中的应用

简介

在 APP 自动化测试中,常常需要与图像进行交互。传统的测试方法通常依赖于文本或图像位置的操作。

然而,随着人工智能技术的发展,Appium 结合了图像识别技术,提供了一个名为 Images 的插件。这个插件可以对识别到的图像进行交互,提取特征,以及比较图像。这些功能的引入不仅增强了测试脚本的功能和适用范围,还使得 App 自动化测试变得更加灵活。

环境安装

安装该插件之前需要提前安装 Nodejs 环境,Appium 服务以及 Appium python 客户端。

本次教程使用的版本为:node.js(v20.12.2)、npm(v10.5.0)、appium(v2.5.4)、Appium-Python-Client(v4.0.0)

注意:nodejs 版本不低于 18.0.0,appium 服务不低于2.x,安装可参考:教程链接

首先,通过以下命令安装插件:

appium plugin install images

然后,在启动 Appium 时,需要确保加上该插件参数:

appium --use-plugins=images

这样就可以在自动化测试过程中使用 Appium 的图像插件。

实践演练

下面将会根据几个示例分别演示 images 插件关于图像比较和图像元素定位和交互的能力。

uml diagram

点击下载 api-demos 的插件:下载地址

在执行用例前需要初始化 driver 实例以及补充 app 应用的相关信息,代码如下:

class TestWeather:
    def setup_method(self):
        # 准备capabilities信息
        caps = {
            "automationName": "UiAutomator2",
            "platformName": "Android",
            "appium:noReset": True,
            "appium:autoUpdateImageElementPosition": True
        }
        appium_server_url = "http://127.0.0.1:4723"
        # 初始化 driver
        self.driver = webdriver.Remote(appium_server_url, options=UiAutomator2Options().load_capabilities(caps))
        self.driver.implicitly_wait(15)

    def teardown_method(self):
        # 退出 app
        self.driver.quit()

具体的使用中会有一些通用的方法,这里提供一个工具类供用例调用,它的作用如下所示:

  • 图片编码处理
  • 图片合并
  • 图片数据获取

代码如下所示:

class Utils:
    IMAGE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "image")

    @classmethod
    def image_to_base64(cls, image_name):
        '''
        将图片进行 base 64 编码
        :param image_name: 传入的图片名称
        :return:
        '''
        image_path = os.path.join(cls.IMAGE_DIR, image_name)
        with open(image_path, 'rb') as f:
            image = base64.b64encode(f.read()).decode('UTF-8')
            return image

    @classmethod
    def base64_to_image(cls, data):
        '''
        将 base64 数据转为图片
        :param data:
        :return:
        '''
        image_data = base64.b64decode(data)
        image = Image.open(BytesIO(image_data))
        image.save(os.path.join(cls.IMAGE_DIR, 'compared_images.png'))
        return image

    @classmethod
    def bind_compare_image(cls, image1_path, image2_path):
        '''
        合并两个图片
        :param image1_path: 图片 1 的路径
        :param image2_path: 图片 2 的路径
        :return:
        '''
        with Image.open(image1_path) as image1, Image.open(image2_path) as image2:
            # 创建一张新的大图
            width = image1.width + 10 + image2.width
            height = max(image1.height, image2.height)
            combined_image = Image.new("RGB", (width, height), "white")

            # 将第一张图片粘贴到大图上
            combined_image.paste(image1, (0, 0))

            # 计算第二张图片的粘贴位置,使其位于中间间隔
            paste_position = (image1.width + 10, 0)
            combined_image.paste(image2, paste_position)

            combined_path = os.path.join(cls.IMAGE_DIR, 'combined_image.png')
            combined_image.save(combined_path)
            # 返回合并后的图片
            return combined_image, image1, image2

    @classmethod
    def draw_label(cls, res, image_combined, image1_size, image2_size=None):
        # 获取 数据
        points1_list = res.get('points1', [])
        points2_list = res.get('points2', [])

        # 画图
        draw = ImageDraw.Draw(image_combined)
        # 遍历20个数据
        for i in random.sample(range(0, len(points1_list) - 1), 20):
            line = [
                (points1_list[i]['x'], points1_list[i]['y']),
                (points2_list[i]['x'] + 10 + image1_size.width, points2_list[i]['y'])
            ]
            draw.line(line, fill='red', width=1)
        image_combined.save(os.path.join(cls.IMAGE_DIR, 'combined_image_with_line.png'))
        return image_combined

    @classmethod
    def draw_rectangle(cls, rec_data, full_image_name, full_image_with_rec):
        # 根据得到的数据画出部分图像的位置
        x = rec_data['rect'].get('x')
        y = rec_data['rect'].get('y')
        width = rec_data['rect'].get('width')
        height = rec_data['rect'].get('height')
        full_image = Image.open(f'./image/{full_image_name}', 'r')
        draw = ImageDraw.Draw(full_image)
        draw.rectangle((x - width, y, x + width, y + 2 * height), outline='red', width=3)
        full_image.show()
        full_image.save(f'./image/{full_image_with_rec}')

图像比较

图像比较结合自动化有多个方面的应用,基本分为三个方面的应用:

  • 计算图片相似度
  • 基于特征比较
  • 查找部分图像

相似度计算

计算已给出两图像之间的相似度并给出计算的结果图,需要注意此时强制要求两个图像的大小相同。

代码示例如下:

def test_calcu_similarity(self):
    # 保存页面截图
    # api demos 的图标截图
    api_icon = Utils.image_to_base64('apidemos_icon.png')
    # 找到对应的 app 并且点击
    self.driver.find_element(AppiumBy.IMAGE, api_icon).click()
    WebDriverWait(self.driver, 15).until(expected_conditions.presence_of_element_located((AppiumBy.XPATH,
                                                                                            "//*[@resource-id='android:id/action_bar']//*[@class='android.widget.TextView']")))
    # 保存 app 首页截图
    self.driver.save_screenshot('./image/api_demos_main.png')
    image1 = Utils.image_to_base64('api_demos_main.png')
    image2 = Utils.image_to_base64('api_demos_main_change.png')
    res = self.driver.get_images_similarity(image1, image2, visualize=True)
    print(f"两张图的相似度为{res['score']}")
    image = Utils.base64_to_image(res['visualization'])
    image.show()
    assert res['score'] > 0.9

基于特征比较

给出已有图像可能出现的特征信息并记录坐标值。适合于两个图像内容相同,但是也许是经过缩放或者旋转的图像

代码示例如下:

def test_compare_image(self):
    # 将图片转码
    image1 = Utils.image_to_base64('compare1.png')
    image2 = Utils.image_to_base64('compare2.png')
    feature_res = self.driver.match_images_features(image1, image2)
    # 提取图片特征
    image_combined, image1, image2 = Utils.bind_compare_image('./image/compare1.png',
                                                                './image/compare2.png')
    Utils.draw_label(feature_res, image_combined, image1, image2)

查找部分图像

在已有的图像当中找到部分特征或者部分图像元素。

代码示例如下:

    def test_find_partial(self):
        # 保存主页面完整截图
        self.driver.save_screenshot('./image/main.png')
        full_main = Utils.image_to_base64('main.png')
        api_icon = Utils.image_to_base64('apidemos_icon.png')
        # 从完整图像当中找部分图像 并且打印出结果
        res = self.driver.find_image_occurrence(base64_full_image=full_main, base64_partial_image=api_icon)
        Utils.draw_rectangle(res,'main.png','image_par.png')

执行结果如下图所示,根据右侧图标图片成功找到页面中对应图像的坐标位置,使用画图标注效果如下图所示:

图像元素定位与交互

基于提供的图像,匹配页面元素,若匹配成功,则会返回用户一个可以交互的标准元素对象,可以像普通元素一样定位和使用它。代码示例如下:

def test_open_api_demos(self):
    # api demos 的图标截图
    api_icon = Utils.image_to_base64('apidemos_icon.png')
    # 找到对应的 app 并且点击
    self.driver.find_element(AppiumBy.IMAGE, api_icon).click()
    # 断言页面内容 是否点击成功
    res = self.driver.find_element(AppiumBy.XPATH,
                                    "//*[@resource-id='android:id/action_bar']//*[@class='android.widget.TextView']").text
    assert res == 'API Demos'

Image 插件的局限性

Images 插件的应用旨在将图像识别技术与 UI 自动化相结合,使得在测试过程中可以更方便地处理图像相关的任务。通过简单的 API 调用,可以获取图像信息,用于图像验证、比对以及直接定位,从而实现快速便捷的 UI 自动化。

然而,可以看出来,Images 插件的准确性受图像识别准确度的影响,是无法完全依赖该插件来实现对不同图像的完美比对。

因此需要结合实际的场景去选择性的使用该插件,可以借助它的功能去辅助目前的工作。

总结

  • 完成 images 插件的安装。
  • 掌握图像比较的功能使用。
  • 掌握图像元素定位的使用。