4. 测试环境搭建:Mock服务、测试数据库、权限数据预置与隔离策略
权限校验失败场景的测试环境,核心挑战在于可控性与隔离性。生产环境中的权限数据复杂、依赖众多,直接测试会导致结果不可复现、干扰频繁。本章围绕三个核心组件——Mock服务、测试数据库、权限数据预置——构建一套可独立运行、快速重置的测试骨架。
4.1 环境架构总览
下图展示了单套测试环境内的组件关系与数据流向。所有外部依赖(认证中心、组织架构服务、第三方鉴权API)均被Mock服务替代,测试数据库独立部署,权限数据通过脚本预置到指定租户/角色下。
设计原则:Mock服务负责“模拟外部鉴权行为”,测试数据库负责“存储权限基线数据”,预置脚本负责“快速重建测试状态”。三者解耦,任一组件可独立升级或替换。
4.2 Mock服务搭建
Mock服务用于替代所有与权限校验相关的外部依赖。推荐使用 WireMock 或 MockServer,以HTTP API形式运行,支持动态响应模板。
4.2.1 核心Mock端点清单
| 依赖服务 | Mock端点 | 典型响应(权限校验失败) | 超时/异常模拟 |
|---|---|---|---|
| 认证中心 | POST /auth/validate |
{"code":401,"msg":"token expired"} |
延迟5s / 返回500 |
| 组织架构服务 | GET /org/user/{id}/roles |
{"code":403,"msg":"user not in any role"} |
连接重置 / 返回空数组 |
| 第三方鉴权API | POST /thirdparty/check |
{"decision":"deny","reason":"ip blocked"} |
返回非JSON / 签名错误 |
4.2.2 WireMock 配置示例
以下为认证中心Mock的Java DSL配置,模拟Token过期场景:
// 模拟Token过期
stubFor(post(urlEqualTo("/auth/validate"))
.withHeader("Authorization", containing("Bearer expired_"))
.willReturn(aResponse()
.withStatus(401)
.withHeader("Content-Type", "application/json")
.withBody("{\"code\":401,\"msg\":\"token expired\"}")));
// 模拟服务超时
stubFor(post(urlEqualTo("/auth/validate"))
.withHeader("Authorization", containing("Bearer timeout_"))
.willReturn(aResponse()
.withFixedDelay(5000)
.withStatus(200)
.withBody("{\"code\":200,\"msg\":\"ok\"}")));
关键策略:Mock响应必须覆盖“正常通过”、“明确拒绝”、“服务异常”、“超时”四类。使用请求头或路径参数区分场景,避免为每个用例启动独立Mock实例。
4.3 测试数据库与权限数据预置
测试数据库采用 H2 内存库(单测)或 PostgreSQL 独立实例(集成测试)。权限数据预置通过 Flyway + 测试种子脚本 实现。
4.3.1 权限表结构(最小模型)
-- 租户表
CREATE TABLE tenant (
id BIGINT PRIMARY KEY,
code VARCHAR(32) UNIQUE NOT NULL
);
-- 用户表
CREATE TABLE user_account (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenant(id),
username VARCHAR(64) NOT NULL,
status VARCHAR(16) DEFAULT 'ACTIVE'
);
-- 角色权限表
CREATE TABLE role_permission (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
resource_path VARCHAR(256) NOT NULL,
action VARCHAR(32) NOT NULL, -- READ, WRITE, DELETE, ADMIN
effect VARCHAR(8) DEFAULT 'DENY' -- ALLOW / DENY
);
4.3.2 预置脚本示例(权限校验失败场景)
-- 租户隔离:租户A有权限,租户B无权限
INSERT INTO tenant(id, code) VALUES (1, 'tenant_a'), (2, 'tenant_b');
-- 用户:租户A下的普通用户,租户B下的管理员
INSERT INTO user_account(id, tenant_id, username) VALUES
(101, 1, 'user_a_normal'),
(102, 2, 'user_b_admin');
-- 权限:仅租户A的admin角色拥有 DELETE 权限
INSERT INTO role_permission(id, role_code, resource_path, action, effect) VALUES
(1001, 'admin', '/api/order/*', 'DELETE', 'ALLOW'),
(1002, 'viewer', '/api/order/*', 'DELETE', 'DENY');
-- 用户角色关联
INSERT INTO user_role(user_id, role_code) VALUES
(101, 'viewer'), -- user_a_normal 只有 viewer 角色 → DELETE 被 DENY
(102, 'admin'); -- user_b_admin 有 admin 角色,但属于租户B
4.3.3 数据隔离策略对比
| 隔离方式 | 实现方法 | 适用场景 | 重置成本 |
|---|---|---|---|
| Schema隔离 | 每个测试类/用例使用独立Schema | 集成测试,高隔离要求 | 中(需动态创建Schema) |
| 租户ID隔离 | 所有数据携带tenant_id,查询强制过滤 | 单库多租户,并行测试 | 低(仅需重置种子数据) |
| 事务回滚 | 测试方法加 @Transactional,结束后回滚 | 单元测试,快速执行 | 极低(自动回滚) |
| 数据库实例隔离 | 每个CI Job启动独立数据库容器 | CI/CD流水线,完全隔离 | 高(需容器编排) |
避坑提示:不要在生产数据库上直接执行权限预置脚本。测试数据库必须独立部署,且每次运行前执行
TRUNCATE + INSERT 重置,避免测试间数据残留导致误判。
4.4 隔离策略与测试夹具(Fixture)管理
权限校验测试的隔离性不仅依赖数据库,还需要在测试代码层面管理上下文。推荐使用 测试夹具工厂 模式,将权限数据预置与Mock配置封装为可复用的Fixture。
4.4.1 夹具设计示例
public class PermissionFixture {
public static void setupTokenExpired(WireMockServer mockAuth) {
mockAuth.stubFor(post(urlEqualTo("/auth/validate"))
.withHeader("Authorization", containing("Bearer expired_"))
.willReturn(aResponse().withStatus(401)
.withBody("{\"code\":401,\"msg\":\"token expired\"}")));
}
public static void setupUserWithoutRole(DataSource ds, Long tenantId, Long userId) {
// 清空该用户的所有角色关联
JdbcTemplate jt = new JdbcTemplate(ds);
jt.update("DELETE FROM user_role WHERE user_id = ?", userId);
// 确保用户存在但无角色
jt.update("MERGE INTO user_account (id, tenant_id, username) VALUES (?, ?, ?)",
userId, tenantId, "no_role_user_" + userId);
}
}
4.4.2 测试用例中的组合使用
@Test
public void testDeleteOrder_withExpiredToken_shouldReturn401() {
// 1. 预置Mock:认证中心返回token过期
PermissionFixture.setupTokenExpired(mockAuthServer);
// 2. 预置数据库:用户存在但无DELETE权限
PermissionFixture.setupUserWithoutRole(dataSource, TENANT_A, USER_101);
// 3. 执行请求(携带过期token)
Response response = given()
.header("Authorization", "Bearer expired_token_abc")
.when().delete("/api/order/123");
// 4. 验证
response.then().statusCode(401);
response.then().body("code", equalTo(401));
}
4.5 环境快速重置脚本
为保障每次测试的隔离性,建议在CI/CD流水线中集成以下重置步骤:
- 停止Mock服务:确保旧stub不残留。
- 重建测试数据库:执行
DROP SCHEMA IF EXISTS test CASCADE; CREATE SCHEMA test;。 - 执行Flyway迁移:应用最新表结构。
- 执行种子脚本:插入权限基线数据(仅包含测试所需的租户、用户、角色)。
- 启动Mock服务:加载默认stub(健康检查端点返回200,其余返回403)。
- 运行测试套件:每个测试类通过Fixture按需覆盖Mock和数据库。
最佳实践:将上述步骤封装为一个
reset-test-env.sh 脚本,在CI Job的 before_script 阶段调用。单次重置耗时控制在30秒以内,确保开发人员本地也能一键执行。