mock测试数据

Mock 测试实战:汽车电池监控调度单元测试

Mock 的核心思想

在本测试中,被测对象 EvBatteryMonitorScheduler 依赖两个外部服务:

依赖作用Mock 方式
IEvBatteryService数据库读取车辆电池列表并提供电池估算@Mock 注解注入,通过 when(...).thenReturn(...) 控制返回值
RestTemplate向外部 API 发送 HTTP 请求创建故障工单@Mock 注解注入,通过 verify(...) 验证调用行为

Mock 的两个核心用途在本测试中的体现:

  1. 隔离依赖(Stub):用 when(batteryService.getVehicles()).thenReturn(...) 模拟数据库返回一组车辆电池数据,让测试完全脱离真实数据库和外部 API。
  2. 行为验证(Verify):用 verify(restTemplate).postForObject(...) 断言被测对象在特定条件下确实调用了或因去重/正常值而未调用创建工单的 HTTP 接口。

并通过注释逐步说明每处 Mock 的用意。

package com.bxsy.modules.ev.schedule;

import com.alibaba.fastjson.JSONObject;
import com.bxsy.modules.ev.entity.EvBattery;
import com.bxsy.modules.ev.service.IEvBatteryService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.http.HttpEntity;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;

import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

/**
 * 汽车动力电池监控调度单元测试(EvBatteryMonitorScheduler)
 *
 * <p>验证电动汽车电池在低续航 / 低健康度场景下是否正确触发故障工单创建 HTTP 调用,
 * 同时通过 Mock 隔离数据库与外部 API 依赖。</p>
 *
 * <h3>Mock 分层策略</h3>
 * <ul>
 *   <li>{@code @Mock IEvBatteryService} —— 替代数据库访问层</li>
 *   <li>{@code @Mock RestTemplate} —— 替代 HTTP 客户端</li>
 * </ul>
 */
@RunWith(MockitoJUnitRunner.class)
public class EvBatteryMonitorSchedulerTest {

    private EvBatteryMonitorScheduler scheduler;

    /**
     * Mock 数据库访问层 —— 所有对车辆电池数据的查询都通过 when-thenReturn 控制
     */
    @Mock
    private IEvBatteryService batteryService;

    /**
     * Mock HTTP 客户端 —— 我们不希望测试时真的去调用故障工单 API
     */
    @Mock
    private RestTemplate restTemplate;

    private static final String BASE_URL = "http://ticket-api.internal:8080/boot";
    private static final String API_KEY = "test-key";

    @Before
    public void setUp() {
        scheduler = new EvBatteryMonitorScheduler();

        // 注入 mock 依赖 —— 被测对象内部通过 setter/字段注入时就用反射塞入 mock
        ReflectionTestUtils.setField(scheduler, "batteryService", batteryService);
        ReflectionTestUtils.setField(scheduler, "restTemplate", restTemplate);
        ReflectionTestUtils.setField(scheduler, "ticketBaseUrl", BASE_URL);
        ReflectionTestUtils.setField(scheduler, "ticketApiKey", API_KEY);

        // 每次测试前清除去重标记
        @SuppressWarnings("unchecked")
        ConcurrentHashMap<String, Long> map =
                (ConcurrentHashMap<String, Long>) ReflectionTestUtils.getField(scheduler, "lastAlarmTime");
        if (map != null) {
            map.clear();
        }
    }

    // ==================== 低续航场景 ====================

    @Test
    public void testLowSocTriggersFaultTicket() {
        // 构造一辆剩余续航仅 45 km 的汽车(阈值 80 km 以下触发)
        EvBattery battery = buildBattery("VIN-001", "Tesla Model 3", "project-A");
        JSONObject batteryResult = buildBatteryResult(45.0, 45.0, 90.0, 1.5);

        // Mock 行为:查询车辆列表返回一辆车
        when(batteryService.getVehicles()).thenReturn(Collections.singletonList(battery));
        // Mock 行为:对这辆车的续航估算返回上面的结果
        when(batteryService.estimateRemainingSoc(eq("VIN-001"), isNull(), isNull()))
                .thenReturn(batteryResult);

        scheduler.checkBatteryHealth();

        // 验证 HTTP 调用 —— ArgumentCaptor 捕获实际发送的 body 做细粒度断言
        ArgumentCaptor<HttpEntity<String>> captor = ArgumentCaptor.forClass(HttpEntity.class);
        verify(restTemplate).postForObject(
                eq(BASE_URL + "/ticket/auto/create"),
                captor.capture(),
                eq(String.class));

        HttpEntity<String> entity = captor.getValue();
        JSONObject body = JSONObject.parseObject(entity.getBody());

        assertEquals("VIN-001", body.getString("vehicleVin"));
        assertEquals("Tesla Model 3", body.getString("model"));
        assertEquals("FLT", body.getString("workOrderType"));
        assertEquals("EV_BATTERY", body.getString("bizType"));
        assertEquals("SCHEDULE", body.getString("source"));
        assertEquals("HIGH", body.getString("priority"));
        assertEquals("III", body.getString("severityLevel"));
        assertTrue(body.getString("title").contains("动力电池续航不足"));

        // extJson 验证
        JSONObject ext = JSONObject.parseObject(body.getString("extJson"));
        assertEquals("EV_BATTERY", ext.getString("faultSource"));
        assertEquals("Tesla Model 3", ext.getString("vehicleModel"));
        assertTrue(ext.getString("faultSymptom").contains("45"));
        assertEquals("BATTERY_DEGRADATION", ext.getString("rootCause"));
        assertEquals("45.0", ext.getString("remainingRangeKm"));
        assertEquals("90.0%", ext.getString("soh"));

        // 验证 header
        assertEquals("application/json",
                entity.getHeaders().getFirst("Content-Type"));
        assertEquals(API_KEY,
                entity.getHeaders().getFirst("X-Internal-Api-Key"));
    }

    // ==================== 低健康度场景 ====================

    @Test
    public void testLowSohTriggersFaultTicket() {
        EvBattery battery = buildBattery("VIN-002", "BYD Han", null);
        // 续航正常(350 km),但 SOH 低(58% < 70% 阈值) → 触发低健康度工单
        JSONObject batteryResult = buildBatteryResult(350.0, 85.0, 58.0, 6.0);

        when(batteryService.getVehicles()).thenReturn(Collections.singletonList(battery));
        when(batteryService.estimateRemainingSoc(eq("VIN-002"), isNull(), isNull()))
                .thenReturn(batteryResult);

        scheduler.checkBatteryHealth();

        ArgumentCaptor<HttpEntity<String>> captor = ArgumentCaptor.forClass(HttpEntity.class);
        verify(restTemplate).postForObject(
                eq(BASE_URL + "/ticket/auto/create"),
                captor.capture(),
                eq(String.class));

        JSONObject body = JSONObject.parseObject(captor.getValue().getBody());

        assertEquals("VIN-002", body.getString("vehicleVin"));
        assertEquals("II", body.getString("severityLevel")); // SOH 低为 II 级
        assertTrue(body.getString("title").contains("电池健康度过低"));

        JSONObject ext = JSONObject.parseObject(body.getString("extJson"));
        assertEquals("58.0%", ext.getString("soh"));
        assertTrue(ext.getString("faultSymptom").contains("58"));
    }

    // ==================== 正常场景不触发 ====================

    @Test
    public void testNormalValuesDoNotTrigger() {
        EvBattery battery = buildBattery("VIN-003", "NIO ET7", "project-B");
        JSONObject batteryResult = buildBatteryResult(500.0, 95.0, 95.0, 10.0); // 一切正常

        when(batteryService.getVehicles()).thenReturn(Collections.singletonList(battery));
        when(batteryService.estimateRemainingSoc(eq("VIN-003"), isNull(), isNull()))
                .thenReturn(batteryResult);

        scheduler.checkBatteryHealth();

        // 不应该调用故障工单 API —— verify(never()) 验证未调用
        verify(restTemplate, never()).postForObject(anyString(), any(), any());
    }

    // ==================== 同时低续航+低健康度 ====================

    @Test
    public void testBothLowTriggersTwoTickets() {
        EvBattery battery = buildBattery("VIN-004", "XPeng P7", "project-C");
        JSONObject batteryResult = buildBatteryResult(30.0, 25.0, 50.0, 0.5); // 两者都低于阈值

        when(batteryService.getVehicles()).thenReturn(Collections.singletonList(battery));
        when(batteryService.estimateRemainingSoc(eq("VIN-004"), isNull(), isNull()))
                .thenReturn(batteryResult);

        scheduler.checkBatteryHealth();

        // 应调用两次(低续航 + 低健康度各一次)—— times(2) 验证调用次数
        verify(restTemplate, times(2)).postForObject(
                eq(BASE_URL + "/ticket/auto/create"),
                any(HttpEntity.class),
                eq(String.class));
    }

    // ==================== 未配置 API 时跳过 ====================

    @Test
    public void testSkipWhenApiNotConfigured() {
        // 清除 baseUrl / apiKey
        ReflectionTestUtils.setField(scheduler, "ticketBaseUrl", "");
        ReflectionTestUtils.setField(scheduler, "ticketApiKey", "");

        EvBattery battery = buildBattery("VIN-005", "理想 L9", null);
        JSONObject batteryResult = buildBatteryResult(20.0, 50.0, 40.0, 0.3);

        when(batteryService.getVehicles()).thenReturn(Collections.singletonList(battery));
        when(batteryService.estimateRemainingSoc(anyString(), isNull(), isNull()))
                .thenReturn(batteryResult);

        scheduler.checkBatteryHealth();

        verify(restTemplate, never()).postForObject(anyString(), any(), any());
    }

    // ==================== 车辆列表为空 ====================

    @Test
    public void testEmptyVehicleList() {
        // Mock 返回空列表
        when(batteryService.getVehicles()).thenReturn(Collections.emptyList());

        scheduler.checkBatteryHealth();

        // 因为没有任何车辆,所以后续的续航估算和 HTTP 调用都不应发生
        verify(batteryService, never()).estimateRemainingSoc(anyString(), any(), any());
        verify(restTemplate, never()).postForObject(anyString(), any(), any());
    }

    // ==================== 去重:6小时内不重复建单 ====================

    @Test
    public void testDedupWithinWindow() {
        EvBattery battery = buildBattery("VIN-006", "AION LX", null);
        JSONObject batteryResult = buildBatteryResult(15.0, 30.0, 80.0, 0.5);

        when(batteryService.getVehicles()).thenReturn(Collections.singletonList(battery));
        when(batteryService.estimateRemainingSoc(eq("VIN-006"), isNull(), isNull()))
                .thenReturn(batteryResult);

        // 第一次调用 → 触发
        scheduler.checkBatteryHealth();
        verify(restTemplate, times(1)).postForObject(anyString(), any(), any());

        // 第二次调用 → 去重,不触发
        scheduler.checkBatteryHealth();
        verify(restTemplate, times(1)).postForObject(anyString(), any(), any()); // 还是1次
    }

    // ==================== 辅助方法 ====================

    private EvBattery buildBattery(String vin, String model, String projectId) {
        EvBattery b = new EvBattery();
        b.setVin(vin);
        b.setModel(model);
        if (projectId != null) {
            b.setProjectId(projectId);
        }
        return b;
    }

    private JSONObject buildBatteryResult(double remainingRangeKm, double soc, double soh,
                                          double estimatedRemainingHours) {
        JSONObject r = new JSONObject();
        r.put("remainingRangeKm", remainingRangeKm);
        r.put("soc", soc);
        r.put("soh", soh);
        r.put("estimatedRemainingHours", estimatedRemainingHours);
        return r;
    }
}

Mock 使用要点总结

在上面的测试中,每条用例都遵循 Arrange-Act-Assert 模式,Mock 集中在 Arrange 和 Assert 两个阶段:

阶段Mock 操作示例
Arrange(准备)when(...).thenReturn(...) 预设 Mock 对象的行为when(batteryService.getVehicles()).thenReturn(...)
Act(执行)调用被测对象的方法scheduler.checkBatteryHealth()
Assert(断言)verify(...) 验证 Mock 对象是否按预期被调用verify(restTemplate, times(2)).postForObject(...)

关键 Mock 技巧:

  1. @Mock + @RunWith(MockitoJUnitRunner.class):自动初始化 Mock 对象,无需手动 Mockito.mock()
  2. ArgumentCaptor:捕获实际传入 HTTP 请求的 body,从而对 JSON 字段做深度断言。
  3. verify(..., never()):断言某个 Mock 方法在特定条件下不应该被调用,验证防御逻辑。
  4. verify(..., times(n)):精确验证调用次数,特别适合验证"同时低续航 + 低健康度生成两张工单"这类场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

所谓远行Misnearch

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值