4. 测试环境搭建:Mock服务、测试数据库、权限数据预置与隔离策略

权限校验失败场景的测试环境,核心挑战在于可控性与隔离性。生产环境中的权限数据复杂、依赖众多,直接测试会导致结果不可复现、干扰频繁。本章围绕三个核心组件——Mock服务、测试数据库、权限数据预置——构建一套可独立运行、快速重置的测试骨架。

4.1 环境架构总览

下图展示了单套测试环境内的组件关系与数据流向。所有外部依赖(认证中心、组织架构服务、第三方鉴权API)均被Mock服务替代,测试数据库独立部署,权限数据通过脚本预置到指定租户/角色下。

权限校验测试环境架构 测试执行器 (JUnit/TestNG) 被测业务系统 (SUT) Mock 服务集群 认证中心 Mock 组织架构 Mock 第三方鉴权 Mock 测试数据库 权限表 (独立Schema) 业务表 (最小数据集) 数据隔离: 租户ID 权限数据预置脚本 测试执行器 被测系统 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));
}
测试夹具(Fixture)组合流程 1. Mock配置 2. 数据库预置 3. 执行请求 4. 断言验证 关键说明: • Mock配置与数据库预置顺序可互换,但必须在请求执行前完成。 • 每个测试用例应只测试一种权限失败原因,避免组合场景导致定位困难。 • 夹具方法应无状态,每次调用都重新stub或重置数据,保证幂等性。

4.5 环境快速重置脚本

为保障每次测试的隔离性,建议在CI/CD流水线中集成以下重置步骤:

  1. 停止Mock服务:确保旧stub不残留。
  2. 重建测试数据库:执行 DROP SCHEMA IF EXISTS test CASCADE; CREATE SCHEMA test;。
  3. 执行Flyway迁移:应用最新表结构。
  4. 执行种子脚本:插入权限基线数据(仅包含测试所需的租户、用户、角色)。
  5. 启动Mock服务:加载默认stub(健康检查端点返回200,其余返回403)。
  6. 运行测试套件:每个测试类通过Fixture按需覆盖Mock和数据库。
最佳实践:将上述步骤封装为一个 reset-test-env.sh 脚本,在CI Job的 before_script 阶段调用。单次重置耗时控制在30秒以内,确保开发人员本地也能一键执行。