Google Maps Java客户端测试指南:TDD、Mockito与WireMock实践
1. 项目概述:为什么我们需要一个完整的Google Maps Java客户端测试套件?
如果你正在开发一个依赖Google Maps API的Java后端服务,比如一个物流调度系统、一个门店位置服务,或者一个地理围栏应用,那么你肯定对“Google Maps Java客户端”这个库不陌生。它封装了与Google Maps Platform服务交互的复杂细节,让我们能用几行Java代码就完成地理编码、路线规划、地点搜索等操作。但麻烦也恰恰在这里:你的业务逻辑和这个外部服务的稳定性深度绑定。想象一下,某个深夜,你的应用突然开始大量报错“ billingnotenabledmaperror ”,或者因为一个API响应格式的微小变动,导致整个地址解析功能瘫痪。更常见的是,在本地开发时一切正常,但到了集成测试或生产环境,才发现API密钥配置、配额限制或网络策略有问题。
这就是为什么我们需要一个 完整、健壮且遵循测试驱动开发(TDD)理念的测试套件 。它不仅仅是为了满足“代码覆盖率”这个数字,更是为了构建一道坚固的防线。这道防线能确保:第一,你对Google Maps API的调用逻辑是正确且符合预期的;第二,当API本身发生变更(比如字段名改变、错误码更新)或你的网络环境、计费状态变化时,你能第一时间发现并定位问题,而不是被用户或监控报警叫醒;第三,在开发新功能或重构旧代码时,你有足够的信心不会破坏现有的地图相关功能。尤其是在面对“ java: outofmemoryerror: insufficient memory ”或“ java: you aren‘t using a compiler supported by lombok ”这类环境问题时,一个好的测试套件能帮你快速区分是环境问题还是业务逻辑问题。
本指南将带你从零开始,为一个典型的Google Maps Java客户端应用,编写一套从单元测试到集成测试的完整测试套件。我们会聚焦于真实开发中的痛点,比如模拟外部API响应、处理异步调用、验证复杂的数据结构(如 LatLng 、 DirectionsRoute ),以及如何优雅地处理各种错误场景。我们的目标是,让你写出的测试代码和业务代码一样可靠、可维护,并且真正成为开发流程中不可或缺的一环。
2. 测试驱动开发(TDD)核心思路与Google Maps场景适配
在深入代码之前,我们必须统一思想:测试驱动开发(TDD)不是先写代码再补测试,而是一种设计工具。它的经典循环是“红-绿-重构”。在Google Maps客户端开发的上下文中,这个循环被赋予了非常具体的内涵。
2.1 TDD循环在外部服务调用中的实践
首先,“红”阶段意味着你要先写一个肯定会失败的测试。这个测试描述了你期望的、最小的功能行为。例如,你正在开发一个“根据地址获取经纬度”的服务。你的第一个测试可能仅仅是:“给定一个有效的地址字符串, GeocodingService 应该返回一个非空的 LatLng 对象”。这时你还没有实现 GeocodingService ,所以运行测试自然是失败的(红色)。这一步的关键在于,它迫使你从调用者的角度去思考接口设计:这个方法叫什么?接收什么参数?返回什么?会抛出什么异常?
接着,进入“绿”阶段。你的目标是用最简单、最直接(甚至可以说是“丑陋”)的代码让这个测试通过。在这个阶段,你可能会在方法里直接硬编码返回一个固定的 LatLng 对象。这没关系!因为TDD的目标不是一步写出完美代码,而是快速得到一个可验证的行为。通过这个测试,你确认了接口契约是可行的。
最后是“重构”阶段。现在测试通过了(绿色),你获得了安全网,可以放心地改进代码内部结构。你会把硬编码替换成真正的Google Maps Java Client API调用。在这个过程中,你可能会发现需要引入依赖注入来管理 GeoApiContext ,或者需要将API密钥外部化。每做一次修改,就运行一次测试套件,确保依然是绿色。这个循环不断重复,功能一点点叠加,代码结构也在测试的保护下持续优化。
2.2 为何Google Maps场景特别适合(也需要)TDD?
- 成本控制 :Google Maps API是按请求次数计费的。在开发调试阶段,如果每次运行应用或测试都发起真实调用,不仅速度慢,还会产生不必要的费用。TDD要求我们频繁运行测试,因此必须对Google Maps客户端进行“模拟”(Mock)或“打桩”(Stub),隔离真实API调用。
- 不确定性管理 :外部API的响应可能因为网络、配额、服务端变更而不稳定。TDD帮助我们定义清晰的“成功”与“失败”边界。我们可以编写测试来验证:当API返回“
ZERO_RESULTS”时,我们的业务逻辑是返回空列表还是抛出特定异常?当返回“OVER_QUERY_LIMIT”时,我们的重试机制是否生效? - 复杂数据结构的验证 :Google Maps API返回的JSON结构非常复杂,反序列化后的Java对象(如
DirectionsResult)包含多层嵌套。TDD鼓励我们从小处着手,例如先测试能否正确解析一个包含单一航路点的路线,再逐步扩展到多航路点、交通方式、避让路径等复杂场景。这能有效避免一次性处理复杂数据时产生的Bug。
注意 :很多开发者在这里会犯一个错误——跳过“红”的阶段,直接写实现和测试,然后同时通过。这失去了TDD最重要的设计反馈环节。务必坚持先写一个明知会失败的小测试,哪怕它看起来微不足道。
3. 环境准备与测试基础设施搭建
工欲善其事,必先利其器。一个高效的测试环境能让你事半功倍。我们将基于Maven项目进行说明,Gradle项目可以找到对应的依赖配置。
3.1 核心依赖引入
在你的 pom.xml 中,除了Google Maps客户端库本身,测试相关的依赖至关重要。
<dependencies>
<!-- Google Maps Services Java Client -->
<dependency>
<groupId>com.google.maps</groupId>
<artifactId>google-maps-services</artifactId>
<version>2.2.0</version> <!-- 请使用最新稳定版 -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
<scope>test</scope> <!-- 测试时使用简单日志 -->
</dependency>
<!-- 测试框架:JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- 模拟框架:Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<!-- 断言库:AssertJ (提供更流畅的断言) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
<!-- 用于在测试中轻松构建JSON Fixture -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
<scope>test</scope>
</dependency>
</dependencies>
3.2 测试资源与配置管理
不要将API密钥硬编码在测试代码中。我推荐使用 src/test/resources 目录下的配置文件。
- 创建
src/test/resources/application-test.properties:# 用于集成测试的真实API密钥(可选,且必须妥善保管) google.maps.api.key=${GOOGLE_MAPS_API_KEY:} # 模拟服务器的基础URL(用于单元测试) google.maps.api.baseurl=http://localhost:8889 - 创建
src/test/resources/logback-test.xml来控制测试时的日志输出,避免过于冗长:<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="WARN"> <!-- 测试时只输出警告和错误,保持整洁 --> <appender-ref ref="STDOUT" /> </root> <logger name="com.google.maps" level="DEBUG"/> <!-- 需要时可单独开启Maps客户端日志 --> </configuration>
3.3 构建测试基类与工具类
一个好的测试基类能封装通用设置。我们创建一个 BaseMapsTest 。
package com.yourcompany.maps.test;
import com.google.maps.GeoApiContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.util.ReflectionTestUtils;
import static org.mockito.Mockito.mock;
@ExtendWith(MockitoExtension.class) // 启用Mockito
@ActiveProfiles("test") // 激活test配置profile
public abstract class BaseMapsTest {
protected GeoApiContext geoApiContext;
protected String testApiKey = "test-api-key-dummy"; // 用于模拟的假密钥
@BeforeEach
void setUpBase() {
// 在实际项目中,这里可以通过@Value注入配置的API密钥
// 对于单元测试,我们通常使用一个模拟的Context或假密钥
geoApiContext = new GeoApiContext.Builder()
.apiKey(testApiKey)
.disableRetries() // 测试中禁用重试,让失败快速暴露
.build();
}
@AfterEach
void tearDownBase() {
if (geoApiContext != null) {
geoApiContext.shutdown();
}
}
/**
* 一个便捷方法,用于读取测试资源文件为字符串,用于模拟API响应。
*/
protected String loadTestResource(String path) throws IOException {
// 实现从src/test/resources加载文件内容
// ...
}
}
实操心得 :在
setUpBase中disableRetries非常重要。Google Maps客户端有内置的重试机制,这在生产环境是好事,但在测试中会掩盖瞬时的网络或模拟服务器问题,导致测试行为不确定(有时成功有时失败)。禁用重试能让测试结果更稳定、反馈更直接。
4. 单元测试策略:隔离与模拟的艺术
单元测试的核心是“隔离”。我们要测试的是 我们的业务逻辑 ,而不是Google Maps服务的可用性。因此,所有对 com.google.maps.GeoApiContext 及其衍生请求(如 DirectionsApi )的调用都应该被模拟。
4.1 使用Mockito模拟Google Maps客户端调用
假设我们有一个简单的服务类 DistanceMatrixService ,它封装了查询距离矩阵的逻辑。
// 生产代码
@Service
public class DistanceMatrixService {
private final GeoApiContext context;
public DistanceMatrixService(GeoApiContext context) {
this.context = context;
}
public long getDrivingDistanceMeters(String origin, String destination) throws Exception {
DistanceMatrixApiRequest req = DistanceMatrixApi.getDistanceMatrix(context,
new String[]{origin},
new String[]{destination});
req.mode(TravelMode.DRIVING);
DistanceMatrix matrix = req.await();
// 简单处理:取第一个结果的距离
if (matrix.rows[0].elements[0].status == DistanceMatrixElementStatus.OK) {
return matrix.rows[0].elements[0].distance.inMeters;
} else {
throw new RuntimeException("Distance calculation failed: " + matrix.rows[0].elements[0].status);
}
}
}
对应的单元测试应该模拟 DistanceMatrixApi 的静态方法。这需要一点技巧,因为Mockito默认不能模拟静态方法。我们可以使用 Mockito的 mockStatic (需要 mockito-inline ) 或者更推荐的做法: 不直接测试静态方法,而是通过依赖注入一个“门面”(Facade)或“包装器”(Wrapper) 。后者是更符合设计原则的做法。
让我们重构一下,引入一个 DistanceMatrixApiWrapper 来封装静态调用:
@Component
public class DistanceMatrixApiWrapper {
public DistanceMatrixApiRequest newRequest(GeoApiContext context, String[] origins, String[] destinations) {
return DistanceMatrixApi.getDistanceMatrix(context, origins, destinations);
}
}
// 服务类改为依赖Wrapper
@Service
public class DistanceMatrixService {
private final GeoApiContext context;
private final DistanceMatrixApiWrapper apiWrapper;
// ... 构造器 ...
public long getDrivingDistanceMeters(String origin, String destination) throws Exception {
DistanceMatrixApiRequest req = apiWrapper.newRequest(context, new String[]{origin}, new String[]{destination});
req.mode(TravelMode.DRIVING);
DistanceMatrix matrix = req.await();
// ... 后续逻辑不变 ...
}
}
现在,单元测试可以轻松模拟 DistanceMatrixApiWrapper :
// 测试代码
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
class DistanceMatrixServiceTest {
private GeoApiContext mockContext;
private DistanceMatrixApiWrapper mockWrapper;
private DistanceMatrixService service;
@BeforeEach
void setUp() {
mockContext = mock(GeoApiContext.class);
mockWrapper = mock(DistanceMatrixApiWrapper.class);
service = new DistanceMatrixService(mockContext, mockWrapper);
}
@Test
void getDrivingDistanceMeters_ShouldReturnDistance_WhenApiCallSucceeds() throws Exception {
// 1. 准备模拟数据
String origin = "Tokyo Station";
String destination = "Tokyo Tower";
DistanceMatrix mockMatrix = new DistanceMatrix();
mockMatrix.rows = new DistanceMatrixRow[] { new DistanceMatrixRow() };
mockMatrix.rows[0].elements = new DistanceMatrixElement[] { new DistanceMatrixElement() };
mockMatrix.rows[0].elements[0].status = DistanceMatrixElementStatus.OK;
mockMatrix.rows[0].elements[0].distance = new Distance();
mockMatrix.rows[0].elements[0].distance.inMeters = 3500L; // 模拟返回3500米
DistanceMatrixApiRequest mockRequest = mock(DistanceMatrixApiRequest.class);
when(mockWrapper.newRequest(mockContext, new String[]{origin}, new String[]{destination}))
.thenReturn(mockRequest);
when(mockRequest.mode(any(TravelMode.class))).thenReturn(mockRequest);
when(mockRequest.await()).thenReturn(mockMatrix);
// 2. 执行测试方法
long distance = service.getDrivingDistanceMeters(origin, destination);
// 3. 验证结果和行为
assertThat(distance).isEqualTo(3500L);
// 可以验证mode被设置为DRIVING
verify(mockRequest).mode(TravelMode.DRIVING);
}
@Test
void getDrivingDistanceMeters_ShouldThrowException_WhenApiReturnsErrorStatus() throws Exception {
// ... 模拟返回状态为 NOT_FOUND 或 ZERO_RESULTS ...
DistanceMatrix mockMatrix = new DistanceMatrix();
mockMatrix.rows = new DistanceMatrixRow[] { new DistanceMatrixRow() };
mockMatrix.rows[0].elements = new DistanceMatrixElement[] { new DistanceMatrixElement() };
mockMatrix.rows[0].elements[0].status = DistanceMatrixElementStatus.NOT_FOUND;
// ... 设置mock ...
// 使用AssertJ的异常断言
assertThatThrownBy(() -> service.getDrivingDistanceMeters("Invalid", "Address"))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Distance calculation failed");
}
}
4.2 验证复杂对象构建与参数传递
有时我们需要验证传递给Google Maps API的参数是否正确。例如,验证是否设置了正确的语言、区域或避让路径。
@Test
void getDrivingDistanceMeters_ShouldSetCorrectParameters() throws Exception {
// ... 模拟设置 ...
// 执行
service.getDrivingDistanceMeters("A", "B");
// 验证:除了mode,还可以验证其他参数
verify(mockRequest).mode(TravelMode.DRIVING);
// 如果服务中设置了语言,可以这样验证
// verify(mockRequest).language("ja");
}
注意事项 :模拟
await()方法时要注意,它声明了抛出Exception。在when(...).thenReturn(...)时,如果模拟方法有受检异常,需要在测试方法签名上声明throws Exception,或者使用thenThrow来模拟异常。这是模拟异步调用结果的关键。
5. 集成测试策略:在可控环境下接触真实API
单元测试保证了业务逻辑的正确性,但无法验证我们与真实的Google Maps Java客户端库的集成是否正确。例如,我们构建的 GeoApiContext 配置是否正确?我们序列化和反序列化数据的逻辑是否与最新版本的客户端库兼容?这就需要集成测试。
5.1 使用WireMock模拟HTTP API
在集成测试中,我们不应该直接调用真实的、计费的Google Maps API。最佳实践是使用 WireMock ,一个用于模拟HTTP服务的库。它允许我们在本地启动一个模拟服务器,并定义当收到特定请求时返回什么响应。
首先,添加WireMock依赖:
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>3.3.1</version>
<scope>test</scope>
</dependency>
然后,编写一个集成测试类。这里的关键是:我们使用一个指向WireMock服务器地址的 GeoApiContext 。
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
class DistanceMatrixServiceIntegrationTest {
// 启动WireMock服务器,监听在8889端口
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().port(8889))
.build();
@Test
void getDrivingDistance_ShouldParseResponseCorrectly_WhenMockServerReturnsSuccess() throws Exception {
// 1. 定义WireMock的行为:当收到特定请求时,返回一个预设的JSON响应
String expectedOrigin = "Sydney Town Hall";
String expectedDestination = "Parramatta, NSW";
wireMock.stubFor(get(urlPathEqualTo("/maps/api/distancematrix/json"))
.withQueryParam("origins", equalTo(expectedOrigin))
.withQueryParam("destinations", equalTo(expectedDestination))
.withQueryParam("mode", equalTo("driving"))
.withQueryParam("key", equalTo("test-integration-key"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(loadTestResource("/responses/distance_matrix_success.json"))));
// 2. 创建指向WireMock的GeoApiContext
GeoApiContext context = new GeoApiContext.Builder()
.apiKey("test-integration-key")
.baseUrlOverride("http://localhost:8889/maps/api/") // 关键:覆盖基础URL
.disableRetries()
.build();
// 3. 实例化真实的服务类(使用真实的Wrapper)
DistanceMatrixApiWrapper realWrapper = new DistanceMatrixApiWrapper();
DistanceMatrixService service = new DistanceMatrixService(context, realWrapper);
// 4. 执行测试
long distance = service.getDrivingDistanceMeters(expectedOrigin, expectedDestination);
// 5. 验证:距离值应该来自我们预设的JSON文件
assertThat(distance).isEqualTo(24280L); // 假设JSON中距离是24280米
// 6. (可选) 验证WireMock确实收到了请求
wireMock.verify(1, getRequestedFor(urlPathEqualTo("/maps/api/distancematrix/json")));
}
@Test
void getDrivingDistance_ShouldThrow_WhenMockServerReturnsApiError() throws Exception {
// 模拟API返回OVER_QUERY_LIMIT错误
wireMock.stubFor(get(urlPathEqualTo("/maps-api/distancematrix/json"))
.willReturn(aResponse()
.withStatus(200) // 注意:Google API错误也返回200,错误在JSON体中
.withBody("{\"status\": \"OVER_QUERY_LIMIT\", \"error_message\": \"You have exceeded your rate limit.\"}")));
GeoApiContext context = new GeoApiContext.Builder()
.apiKey("test-key")
.baseUrlOverride("http://localhost:8889/maps/api/")
.build();
DistanceMatrixService service = new DistanceMatrixService(context, new DistanceMatrixApiWrapper());
// 验证服务是否抛出了预期的异常
assertThatThrownBy(() -> service.getDrivingDistanceMeters("A", "B"))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Distance calculation failed");
}
}
其中, /responses/distance_matrix_success.json 文件内容是从真实Google Maps API响应中保存下来的一个样例(记得移除敏感信息):
{
"destination_addresses": [ "Parramatta NSW, Australia" ],
"origin_addresses": [ "Sydney Town Hall NSW, Australia" ],
"rows": [
{
"elements": [
{
"distance": { "text": "24.3 km", "value": 24280 },
"duration": { "text": "40 mins", "value": 2423 },
"status": "OK"
}
]
}
],
"status": "OK"
}
5.2 测试错误与异常场景
集成测试是验证错误处理的绝佳场所。你需要测试各种Google Maps API可能返回的错误状态,如:
OVER_QUERY_LIMIT:配额超限。REQUEST_DENIED:API密钥无效或未启用计费(billingnotenabledmaperror的根源)。INVALID_REQUEST:请求参数缺失或格式错误。UNKNOWN_ERROR:服务器端未知错误。
为每一种错误状态编写一个测试用例,确保你的服务能够以符合业务需求的方式处理它们(例如,抛出特定业务异常、触发告警、或执行重试)。
实操心得 :维护一份真实的API响应JSON文件作为测试夹具(Fixture)非常有用。你可以使用浏览器开发者工具或Postman调用一次真实的API(在开发环境下,小心配额),将成功的、各种错误的响应保存下来。这保证了你的解析逻辑与当前API版本同步,并且测试不依赖于网络。
6. 测试套件组织与持续集成
当测试用例越来越多,如何组织和管理它们就变得很重要。
6.1 使用JUnit 5的标签(Tag)进行分类
你可以使用 @Tag 注解来区分测试类型,然后在Maven或Gradle中选择性地运行。
// 单元测试,快速,不依赖外部服务
@Tag("unit")
class DistanceMatrixServiceUnitTest { /* ... */ }
// 集成测试,较慢,需要启动WireMock
@Tag("integration")
class DistanceMatrixServiceIntegrationTest { /* ... */ }
// 一个可能存在的、需要真实网络和API密钥的端到端测试(慎用,通常不在CI中运行)
@Tag("e2e")
@Disabled("Requires real API key and network, run manually only")
class DistanceMatrixServiceE2ETest { /* ... */ }
在Maven的 pom.xml 中配置Surefire插件来运行不同标签的测试:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<!-- 默认运行所有非Integration的测试 -->
<groups>unit</groups>
</configuration>
</plugin>
6.2 在CI/CD流水线中运行测试
在持续集成(如Jenkins、GitHub Actions、GitLab CI)中,通常的步骤是:
- 编译阶段 :运行所有
@Tag(“unit”)测试。必须快速通过,这是代码合并的第一道关卡。 - 集成测试阶段 :在构建环境里,运行所有
@Tag(“integration”)测试。这个环境需要能启动WireMock服务器(通常没问题)。 - (可选)端到端测试阶段 :在一个更接近生产的环境里,使用受控的、有足够配额的测试专用API密钥,运行少量关键的
@Tag(“e2e”)测试。这个阶段可能不稳定且消耗配额,需要谨慎管理。
6.3 测试数据管理与清理
对于集成测试,确保每个测试都是独立的,不会相互影响。使用 @BeforeEach 或 @AfterEach 来重置WireMock的状态( wireMock.resetAll() ),并确保使用不同的API密钥或请求参数,避免测试间的意外耦合。
7. 高级场景与疑难问题排查
7.1 测试异步与非阻塞调用
Google Maps Java客户端主要是同步的(使用 await() ),但如果你在其之上封装了异步调用(如使用 CompletableFuture ),测试需要相应调整。可以使用 Mockito 的 ArgumentCaptor 来捕获回调,或者使用 CompletableFuture 的 get() 方法(在测试中设置合理超时)来获取结果进行断言。
7.2 处理速率限制和重试逻辑
如果你的服务实现了自定义的重试逻辑(例如,遇到 OVER_QUERY_LIMIT 后等待一段时间再重试),测试会变得复杂。你需要模拟一个在第一次调用失败、第二次调用成功的场景。这可以通过在WireMock中定义 情景(Scenario) 来实现:
wireMock.stubFor(get(urlPathEqualTo("/maps/api/geocode/json"))
.inScenario("Retry Scenario")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(aResponse().withStatus(200).withBody(overQueryLimitResponse))
.willSetStateTo("Retried Once"));
wireMock.stubFor(get(urlPathEqualTo("/maps/api/geocode/json"))
.inScenario("Retry Scenario")
.whenScenarioStateIs("Retried Once")
.willReturn(aResponse().withStatus(200).withBody(successResponse)));
然后你的测试需要触发两次调用,并验证最终结果是成功的。
7.3 常见问题排查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
单元测试中 NullPointerException 在 await() 后 |
模拟的 DistanceMatrixApiRequest 对象或其返回的 DistanceMatrix 对象未完全构建或为 null 。 |
检查模拟设置,确保 when(mockRequest.await()).thenReturn(...) 返回的是一个完全初始化(所有必要字段不为null)的模拟对象。使用 Mockito.mock(Class, Answers.RETURNS_DEEP_STUBS) 可以简化深层模拟,但需谨慎使用。 |
| 集成测试失败,提示连接被拒绝 | WireMock服务器未启动或端口被占用。 | 1. 检查 @RegisterExtension 是否正确。2. 确保测试中使用的端口(如8889)与 baseUrlOverride 一致。3. 检查是否有其他进程占用了该端口。 |
测试通过,但真实调用失败并报 billingnotenabledmaperror |
测试使用的模拟响应未覆盖此错误,或真实API密钥未启用计费。 | 1. 在集成测试中增加针对 REQUEST_DENIED 状态和此特定错误信息的测试用例。2. 检查生产/测试环境的API密钥在Google Cloud Console中是否已启用计费并关联了正确的项目。 |
测试时出现 java: outofmemoryerror: insufficient memory |
测试套件规模大,或单个测试加载了大量资源(如巨大的JSON夹具)。 | 1. 调整JVM测试运行内存 ( -Xmx512m )。2. 检查测试夹具文件大小,确保没有误提交生产数据。3. 使用 @BeforeAll / @AfterAll 替代 @BeforeEach 来共享重型资源,减少开销。 |
| Mockito模拟静态方法失败 | 使用了旧版Mockito或未配置 mockito-inline 。 |
对于静态方法模拟,添加依赖 org.mockito:mockito-inline 。但更推荐使用“包装器”模式,它使代码更可测试、更清晰。 |
7.4 性能与并发测试
对于高并发使用地图服务的应用,还需要考虑性能测试。虽然这超出了单元/集成测试范畴,但可以在集成测试中初步验证:在循环中快速发起多个请求到WireMock,观察你的服务是否有资源泄漏(如 GeoApiContext 未关闭)、线程池配置是否合理。可以使用 @RepeatedTest 或专门的性能测试框架(如JMH)进行更深入的评估。
编写Google Maps Java客户端的测试套件,初看似乎增加了不少工作量,但它所带来的信心和稳定性提升是巨大的。它让你能坦然面对API的变更,从容地进行重构,并快速定位生产环境中的问题。从今天开始,尝试为你的下一个地图相关功能,先写一个失败的测试吧。
更多推荐
所有评论(0)