多车型、多品牌的自动化适配策略

4 阅读6分钟

一套框架覆盖 5 款车,适配层只写了 200 行代码。核心不是"写得少",是你抽象对了东西。


引子

去年年底,测试组接到一个任务:APP 自动化框架要覆盖 3 个品牌、5 款车型,从 A 级轿车到全尺寸 SUV,年款跨度 3 年,配置组合超过 40 种。

团队的第一反应是:每款车单独建一个测试项目。这意味着 5 套 Page Object、5 套用例数据、5 套 CI 流水线。维护成本先不谈——每次 APP 发版,光回归验证就要跑 5 遍。

我按住了这个方向。不是因为工作量——而是它在一开始就埋了一个更严重的坑:测试逻辑被车型耦合了。


核心矛盾:车型差异到底是"数据不同"还是"行为不同"

先想清楚一件事:APP 自动化测试里,车型差异究竟体现在哪?

举个例子:远程开空调。Model A 支持温度分区控制(主驾/副驾独立),Model B 只有单区温控,Model C 连空调都是手动的。如果你给每款车写一套测试用例,你的 case 会长成三种完全不同的东西,因为"开空调"这个行为的入参和验证点都不一样。

但如果换个思路——

车型差异不是"测试逻辑不同",是"能力集不同"。开空调这件事本身,测试流程是一样的:发送指令 → 等待执行回执 → 校验 APP 状态更新。区别只在于:这个车有没有分区、有多少个分区、温度范围是多少。

把这个问题建模清楚之后,适配层的设计就自然出来了。


三层适配架构

我设计的方案分三层:

用例层(test case)
    ↓ 调用抽象动作
能力抽象层(VehicleCapability)
    ↓ 根据 VIN/ConfigCode 查表
车型配置映射(VehicleProfile)

第一层:VehicleCapability —— 能力抽象

不直接测"某款车的空调",而是定义一个能力接口:

class ClimateCapability(ABC):
    @abstractmethod
    def supported_zones(self) -> list[str]:
        """返回支持的温度分区,如 ['driver', 'passenger', 'rear']"""
        pass

    @abstractmethod
    def temperature_range(self) -> tuple[int, int]:
        """返回温度调节范围"""
        pass

    @abstractmethod
    def ac_mode(self) -> str:
        """'auto' | 'manual' | 'dual_zone'"""
        pass

每个车型实现自己的能力声明。Model A 返回 ['driver', 'passenger'],Model B 返回 ['driver'],Model C 直接返回 []——连空调都没有,用例层跳过相关测试。

第二层:VehicleProfile —— 配置映射

这是个 KV 表,key 是 brand_model_year_config,value 是能力声明的具体参数:

profiles:
  brand_x_model_3_2023_high:
    climate:
      zones: [driver, passenger]
      temp_range: [16, 32]
      ac_mode: dual_zone
    sunroof: panoramic
    seats: [heated, ventilated]
    
  brand_x_model_3_2023_base:
    climate:
      zones: [driver]
      temp_range: [18, 30]
      ac_mode: auto
    sunroof: standard
    seats: []
    
  brand_y_suv_2024:
    climate:
      zones: []
    sunroof: none
    seats: [heated]

关键设计原则:Profile 层只声明"有什么、能做什么",不包含任何测试逻辑。测试逻辑全部留在用例层。

第三层:用例层 —— 动态生成

用例不再硬编码车型参数,而是读能力集来决定跑什么:

def test_climate_control(vehicle_profile):
    cap = vehicle_profile.climate
    
    if not cap.supported_zones():
        pytest.skip("车型不支持远程空调")
    
    for zone in cap.supported_zones():
        result = set_temperature(zone, target=24)
        assert result.status == "executed"
        assert vehicle_app.get_climate_display(zone) == 24

同一套 case,跑在不同车型上,自动执行该车型能力范围内的事。Model C 直接跳过,不产生失败。


适配层的维护成本怎么控制

三层架构听上去美好,但有人会问:每新上一个车型都要写 Profile 和 Capability 实现,这不还是手写?

是的——但成本是线性的,不是组合爆炸的。

Profile 文件本质是一个声明式的 YAML。新增一个配置型号,加 20 行 YAML,不碰代码。Capability 实现只在"车型引入了新能力类型"时才需要写新的抽象接口。而事实上,车端能力域的扩展速度远低于车型数量的增长速度——过去一年我们增加了 4 款新车型,只新增了一个 Capability 接口(数字钥匙 CCC 标准)。

真正的维护大头在配置校验。我们引入了一个 CI 前置检查:

def test_profile_consistency():
    """每次构建前校验所有 Profile 的合法性"""
    for profile in load_all_profiles():
        assert profile.version in SUPPORTED_VERSIONS
        for capability in profile.capabilities:
            assert capability in REGISTERED_CAPABILITIES
            assert capability.validate(profile[capability.name])

这避免了"Profile 写错了字段名导致用例静默跳过"的问题——这个坑我们踩过一次,定位花了一个下午。


真实踩坑

说三个踩过的:

1. 不同品牌的车,同一个功能叫不同的名。

座椅加热,品牌 A 的 API 字段叫 seat_heat,品牌 B 叫 seat_warmer。你在 Capability 抽象层做了统一命名(seat_heating),适配层负责翻译。但别在用例层直接写品牌 A 的字段名——一旦耦合,品牌 B 上来全炸。

2. "没有这个功能"和"这个功能坏了"长得一模一样。

Model C 不支持远程空调,API 返回错误码。你怎么判断是"这车本来就没有"还是"服务器挂了"?解法是在 Profile 里预先声明能力集,任何未声明能力的 API 调用返回异常时,用例框架先查 Profile——不在能力集内 → 跳过;在能力集内 → 报失败。

3. 年款差异比品牌差异更麻烦。

同一款车的 2023 款和 2024 款,中控升级了芯片,UI 布局变了。图像识别的定位坐标全得改。这不是适配层能解决的——图像定位的坐标是属于"车型 × 年款 × 屏幕分辨率"三维映射表的。解法:给每个 Profile 挂一个 ui_layout_version 字段,图像定位坐标按 layout version 索引,跟 Profile 一起维护。


总结

多车型适配的核心不是"省事",是把"车型是什么"和"怎么测"解耦。

三层架构的值在于:新增一辆车,你只改 Profile 声明;新增一种能力类型,你才需要改 Capability 抽象;用例逻辑完全不动。

具体落地建议三步走:先在已有框架里找 3 款差异最大的车做适配试点 → 提炼出第一版 Capability 抽象接口 → 新车型接入时用 Profile 驱动,不做二次开发。前两步慢,第三步会快到你自己都意外。


[APP测试] #自动化测试 #车联APP #测试框架 #多车型适配

下一篇预告: 智能驾驶仿真场景库构建与自动化注入——场景不是越多越好,讲参数化场景生成的真实方法。