对应代码:配套代码/test/utils/visual_comparison.py
说明:本节讲解如何通过截图对比来验证 UI 的视觉一致性,包括视觉回归测试的实现。
这节讲什么
UI 测试通常验证两件事:
- 功能对不对:按钮能不能点、页面能不能跳转、数据显不显示
- 长得好不好:颜色对不对、布局对不对、元素有没有错位
功能测试好做------用断言检查元素状态就行。但视觉测试难做------"长得好不好"是主观判断,代码怎么验证?
视觉测试就是解决第二个问题:通过截图对比,自动发现 UI 的视觉变化。
核心思路
视觉测试的基本流程:
截图 → 与设计稿对比 → 计算相似度 → 生成差异图 → 判断是否通过
对比方法
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 像素对比 | 逐像素比较 | 精确 | 对微小变化敏感(抗锯齿、字体渲染) |
| SSIM | 结构相似性 | 更符合人眼感知 | 计算量大 |
| 特征点匹配 | 匹配 SIFT/ORB 特征 | 对缩放、旋转鲁棒 | 复杂场景误匹配 |
我选择像素对比 + SSIM组合:
- 像素对比用于快速判断"有没有变化"
- SSIM 用于判断"变化大不大"(更符合人眼感知)
阈值设定
# 相似度阈值
SIMILARITY_THRESHOLD = 0.95 # SSIM 相似度 >= 0.95 认为通过
# 像素差异阈值
PIXEL_DIFF_THRESHOLD = 0.01 # 差异像素比例 <= 1% 认为通过
为什么设 0.95?因为:
- 0.99 太严格:抗锯齿、字体渲染的微小差异都会导致失败
- 0.8 太宽松:明显的视觉变化可能漏掉
- 0.95 是经验值:能发现真正的视觉问题,又不会因为微小差异误报
实战案例
案例 1:登录页视觉回归
# 1. 截图
screenshot = driver.get_screenshot_as_png() # 返回 bytes,不是布尔值
import io
import numpy as np
arr = np.frombuffer(screenshot, np.uint8)
screenshot_img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
# 2. 加载设计稿
design = cv2.imread("designs/login_page.png")
# 3. 对比
similarity = calculate_ssim(screenshot_img, design)
# 4. 判断
if similarity < 0.95:
# 生成差异图
diff = generate_diff_image(screenshot_img, design)
diff.save("diffs/login_page_diff.png")
raise AssertionError(f"视觉回归失败,相似度: {similarity}")
案例 2:多页面批量对比
# 定义需要对比的页面
pages = ["login", "home", "profile", "settings"]
for page in pages:
# 截图
screenshot = capture_page(page)
# 对比
similarity = compare_with_design(screenshot, page)
# 记录结果
results[page] = {
"similarity": similarity,
"passed": similarity >= 0.95
}
代码实现
核心代码在 utils/visual_comparison.py:
class VisualComparison:
def __init__(self, design_dir="testcases/ui_designs/figma_exports"):
self.design_dir = Path(design_dir)
self.design_images = {}
self._load_design_images() # 加载所有设计稿
def compare(self, screenshot_path, design_name):
"""对比截图与设计稿"""
# 1. 加载图片
screenshot = cv2.imread(screenshot_path)
design = self._find_design(design_name)
# 2. 调整尺寸一致
screenshot = self._resize_to_match(screenshot, design)
# 3. 计算 SSIM
similarity = ssim(screenshot, design, multichannel=True)
# 4. 判断
if similarity < 0.95:
# 生成差异图
diff = self._generate_diff(screenshot, design)
diff_path = f"diffs/{design_name}_diff.png"
cv2.imwrite(diff_path, diff)
raise AssertionError(f"视觉回归失败,相似度: {similarity:.4f}")
return similarity
注意事项
1. 设计稿的获取
视觉测试的前提是有设计稿。设计稿可以从 Figma/Sketch 导出,也可以从首次运行的截图生成(作为基线)。
建议:
- 有设计稿:直接用设计稿对比(最准确)
- 无设计稿:首次运行截图作为基线,后续对比基线(视觉回归)
2. 环境差异的影响
不同设备、不同系统版本的截图会有差异:
- Android 和 iOS 的字体渲染不同
- 不同分辨率的屏幕截图尺寸不同
- 不同 Appium 版本的截图质量不同
建议:
- 对比前统一截图尺寸(resize 到设计稿尺寸)
- 使用同一台设备跑回归测试
- 设置合理的阈值(0.95),避免微小差异误报
3. 动态内容的处理
有些内容是动态的(时间、随机数、广告),每次截图都不一样。
建议:
- 对比前屏蔽动态区域(用黑色矩形覆盖)
- 或者只对比静态区域(如导航栏、按钮)
- 动态内容不适合做视觉测试
4. 性能开销
SSIM 计算比较耗时(每张图 0.5-2 秒),如果页面多,整体测试时间会显著增加。
建议:
- 只对核心页面做视觉测试(登录、首页、支付页)
- 非核心页面用功能测试覆盖
- 视觉测试放在 CI 的夜间构建中,不阻塞主流程
视觉测试的局限性
视觉测试听起来很美好,但在实际落地时会遇到几个绕不开的问题:
设计稿从哪来?
- 理想情况:设计团队用 Figma/Sketch 出稿,测试直接导出 PNG 对比
- 现实情况:很多项目没有设计稿,或者设计稿和实际 UI 不一致(开发改了但没更新设计稿)
- 没有设计稿时的替代方案:首次运行截图作为基线,后续对比基线(视觉回归)
谁负责维护设计稿?
- 设计稿不是写一次就完事的------每次 UI 改版都需要更新设计稿
- 如果设计稿更新不及时,视觉测试会大量误报
- 建议:把设计稿更新纳入 UI 改版的流程中,开发改 UI 的同时必须更新设计稿
哪些页面值得做视觉测试?
- 值得:登录页、首页、支付页(核心页面,改版频繁)
- 不值得:设置页、关于页(很少改版,功能测试就够了)
- 不建议:动态内容页面(时间、广告、个性化推荐------每次都不一样)
总结一句话:视觉测试的价值取决于设计稿的质量和维护频率。如果设计稿没人管,视觉测试就会变成"误报制造机"。