AllraTestWritingStandardsSkill allra-test-writing

后端测试编写标准,涵盖测试助手选择、测试数据生成、测试模式应用、断言验证等关键环节,旨在提升测试效率和质量。

测试 0 次安装 0 次浏览 更新于 3/1/2026

Allra 测试编写标准

Allra 后端团队的测试编写标准定义。包括测试助手选择、Fixture Monkey 数据生成、Given-When-Then 模式、AssertJ 验证等。

项目基本信息

本指南基于以下环境编写:

  • Java: 17 以上
  • Spring Boot: 3.2 以上
  • 测试框架: JUnit 5
  • 断言库: AssertJ
  • 模拟: Mockito
  • 测试数据: Fixture Monkey(可选)
  • 容器: Testcontainers(可选)

参考: 项目使用的具体库或版本可能有所不同。根据项目进行调整使用。

测试助手选择指南

注意: 下面测试助手由 Allra 标准模板提供。如果项目中没有这些助手,直接使用 Spring Boot 基本测试注解(@SpringBootTest, @DataJpaTest, @WebMvcTest 等),但要遵循本指南的测试模式和原则。

助手 标签 用途 重量 何时?
IntegrationTest Integration 多个服务集成 🔴 重 整个工作流程
RdbTest RDB Repository, QueryDSL 🟡 中等 查询验证
ControllerTest Controller API 端点 🟢 轻 REST API 验证
RedisTest Redis Redis 缓存 🟢 轻 缓存验证
MockingUnitTest MockingUnit Service 单元 🟢 非常轻 业务逻辑
PojoUnitTest PojoUnit 领域逻辑 🟢 非常轻 纯 Java

选择流程

API 端点?→ ControllerTest
多个服务集成?→ IntegrationTest
Repository/QueryDSL?→ RdbTest
Redis 缓存?→ RedisTest
Service 逻辑(Mock)?→ MockingUnitTest
领域逻辑(POJO)?→ PojoUnitTest

🎯 Mock vs Integration 选择标准(重要!)

原则: 基本是 MockingUnitTest,仅在必要时使用 IntegrationTest

目标: 保持 IntegrationTest 比例在 5% 以下

决策流程图

┌─────────────────────────────────┐
│ 测试的目标是什么?    │
└────────────┬────────────────────┘
             │
    ┌────────▼────────┐
    │ 仅领域逻辑?  │ ──Yes──> PojoUnitTest
    └────────┬────────┘
             │ No
    ┌────────▼─────────────────────┐
    │ Repository/QueryDSL 查询?   │ ──Yes──> RdbTest
    └────────┬─────────────────────┘
             │ No
    ┌────────▼─────────────────────┐
    │ API 端点响应/验证?   │ ──Yes──> ControllerTest
    └────────┬─────────────────────┘
             │ No
    ┌────────▼─────────────────────────────┐
    │ Service 业务逻辑验证?         │
    └────────┬─────────────────────────────┘
             │
    ┌────────▼──────────────────────────────────────────┐
    │ 下列情况是否适用?                      │
    │                                                   │
    │ 1. 💰 金钱处理(存款/取款/转账/退款)            │
    │ 2. 🔄 事务回滚重要的工作流程           │
    │ 3. 📊 多个表数据一致性验证             │
    │ 4. 🔐 实际 DB 约束条件验证必须                 │
    │ 5. 📝 复杂的状态转移(3 阶段以上)              │
    │ 6. 🎯 事件发布/监听器集成验证               │
    │ 7. 🤝 3 个以上服务必须合作                  │
    └────┬──────────────────────────────────────┬────────┘
         │ Yes                                  │ No
         │                                      │
    ┌────▼────────────┐              ┌─────────▼──────────┐
    │ IntegrationTest │              │ MockingUnitTest    │
    │ (最小化)        │              │ (基本选择)       │
    └─────────────────┘              └────────────────────┘

需要 IntegrationTest 的具体案例

✅ 1. 金钱处理(存款/取款/转账/退款)

理由: 与金钱相关的逻辑必须验证实际 DB 事务操作

// 示例:众筹申请(FsData → FsPayment → PointUsage → UserAccount 联动)
@DisplayName("众筹申请时金额扣除及支付创建")
class ApplyServiceIntegrationTest extends IntegrationTest {

    @Test
    @Transactional
    void apply_DecreasesAmount_Success() {
        // given: 用户余额 100 万
        User user = createUserWithBalance(1_000_000);

        // when: 50 万众筹申请
        applyService.apply(new ApplyRequest(user.getId(), 500_000));

        // then: 实际 DB 中余额 50 万确认
        User updated = userRepository.findById(user.getId()).get();
        assertThat(updated.getBalance()).isEqualTo(500_000);

        // then: FsPayment 创建确认
        FsPayment payment = fsPaymentRepository.findByUserId(user.getId()).get();
        assertThat(payment.getAmount()).isEqualTo(500_000);
    }
}

✅ 2. 事务回滚重要的工作流程

理由: 失败时所有操作必须原子性回滚

// 示例:支付失败时全部回滚
@Test
@DisplayName("支付失败时申请数据也回滚")
void apply_PaymentFails_RollbackAll() {
    // given
    User user = createUser();
    mockPaymentGateway_ToFail(); // 外部支付是 Mock

    // when & then
    assertThatThrownBy(() -> applyService.apply(request))
        .isInstanceOf(PaymentException.class);

    // then: DB 中没有任何数据保存
    assertThat(fsDataRepository.findAll()).isEmpty();
    assertThat(fsPaymentRepository.findAll()).isEmpty();
}

参考: 外部对接(支付网关,外部 API)用 @MockBean 处理

✅ 3. 多个表数据一致性验证

理由: 确认所有相关表的状态一致性

// 示例:创建合同时 UserAccount, Contract, FsData 都创建
@Test
@DisplayName("新建合同时相关表都创建")
void createContract_CreatesAllRelatedData() {
    // when
    contractService.createContract(userId, contractType);

    // then: 3 个表都有数据存在
    assertThat(userAccountRepository.findByUserId(userId)).isPresent();
    assertThat(contractRepository.findByUserId(userId)).isPresent();
    assertThat(fsDataRepository.findByUserId(userId)).isPresent();
}

✅ 4. 实际 DB 约束条件验证

理由: Unique, FK, Check 约束只能在实际 DB 中确认

// 示例:防止重复账户注册
@Test
@DisplayName("相同账户号重复注册时异常")
void registerAccount_Duplicate_ThrowsException() {
    // given
    userAccountRepository.save(new UserAccount(userId, "123-456-789"));

    // when & then: Unique 约束违反
    assertThatThrownBy(() ->
        userAccountRepository.save(new UserAccount(userId, "123-456-789"))
    ).isInstanceOf(DataIntegrityViolationException.class);
}

✅ 5. 复杂的状态转移(3 阶段以上)

理由: 按实际场景验证状态变化流程

// 示例:合同状态转移(申请 → 审核 → 批准 → 完成)
@Test
@DisplayName("合同工作流程全部验证")
void contractWorkflow_FullCycle() {
    // given: 申请
    Contract contract = contractService.create(userId);
    assertThat(contract.getStatus()).isEqualTo(ContractStatus.PENDING);

    // when: 审核
    contractService.review(contract.getId());
    // then
    Contract reviewed = contractRepository.findById(contract.getId()).get();
    assertThat(reviewed.getStatus()).isEqualTo(ContractStatus.REVIEWED);

    // when: 批准
    contractService.approve(contract.getId());
    // then
    Contract approved = contractRepository.findById(contract.getId()).get();
    assertThat(approved.getStatus()).isEqualTo(ContractStatus.APPROVED);
}

✅ 6. 事件发布/监听器集成验证

理由: 确认事件实际发布并且监听器工作

// 示例:合同完成事件 → 通知发送
@Test
@DisplayName("合同完成时通知事件发布")
void completeContract_PublishesEvent() {
    // given
    Contract contract = createContract(userId);

    // when
    contractService.complete(contract.getId());

    // then: 实际通知是否发送?(外部通知是 @MockBean)
    verify(notificationService).sendContractCompleteNotification(userId);
}

✅ 7. 3 个以上服务必须合作

理由: 在实际环境中验证服务间互动

// 示例:下单 → 库存减少 → 支付 → 通知
@Test
@DisplayName("下单工作流程")
void createOrder_FullWorkflow() {
    // given
    Product product = createProductWithStock(100);

    // when
    orderService.createOrder(userId, product.getId(), 10);

    // then: 库存减少
    Product updated = productRepository.findById(product.getId()).get();
    assertThat(updated.getStock()).isEqualTo(90);

    // then: 支付创建
    Payment payment = paymentRepository.findByUserId(userId).get();
    assertThat(payment.getStatus()).isEqualTo(PaymentStatus.COMPLETED);
}

MockingUnitTest 足够案例

✅ 大部分 Service 逻辑

  • 简单查询(findById, findAll)
  • 数据转换/计算
  • 验证逻辑(validation)
  • 单个实体 CRUD
  • 业务规则验证
// 示例:折扣率计算逻辑(Mock 足够)
@ExtendWith(MockitoExtension.class)
class DiscountServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private DiscountService discountService;

    @Test
    @DisplayName("VIP 用户 10% 折扣计算")
    void calculateDiscount_VipUser_10Percent() {
        // given
        User vipUser = User.builder().grade("VIP").build();
        when(userRepository.findById(1L)).thenReturn(Optional.of(vipUser));

        // when
        BigDecimal discount = discountService.calculateDiscount(1L, new BigDecimal("10000"));

        // then
        assertThat(discount).isEqualByComparingTo(new BigDecimal("1000"));
    }
}

外部对接处理原则

重要: IntegrationTest 中也用 @MockBean 处理外部系统

@SpringBootTest
class PaymentServiceIntegrationTest extends IntegrationTest {

    @Autowired
    private PaymentService paymentService;

    @MockBean // 外部支付网关是 Mock
    private ExternalPaymentGateway externalPaymentGateway;

    @MockBean // 外部通知服务是 Mock
    private ExternalNotificationService notificationService;

    @Test
    @DisplayName("支付成功时内部数据一致性验证")
    void processPayment_Success() {
        // given: 外部支付成功 Mock
        when(externalPaymentGateway.charge(any()))
            .thenReturn(new PaymentResult("SUCCESS", "tx-123"));

        // when: 实际内部逻辑验证
        paymentService.processPayment(userId, amount);

        // then: 内部 DB 状态确认
        Payment payment = paymentRepository.findByUserId(userId).get();
        assertThat(payment.getStatus()).isEqualTo(PaymentStatus.COMPLETED);
        assertThat(payment.getExternalTxId()).isEqualTo("tx-123");
    }
}

测试策略总结

测试类型 目标比例 执行速度 主要用途
PojoUnitTest 30% ⚡️ 0.01秒 领域逻辑,工具类
MockingUnitTest 50% ⚡️ 0.1秒 Service 业务逻辑
ControllerTest 10% 🟡 0.5秒 API 验证
RdbTest 5% 🟡 1秒 复杂查询验证
IntegrationTest 5% 🔴 5秒 金钱/事务/工作流程

快速判断检查列表

编写新测试时请确认以下内容:

□ 涉及金钱吗?(存款/取款/支付)
  → Yes: IntegrationTest

□ 失败时数据回滚重要吗?
  → Yes: IntegrationTest

□ 需要确认 3 个以上表的一致性吗?
  → Yes: IntegrationTest

□ DB 约束(Unique/FK)是核心吗?
  → Yes: IntegrationTest

□ 需要验证复杂的状态转移(3 阶段+)吗?
  → Yes: IntegrationTest

□ 需要验证事件发布/监听器吗?
  → Yes: IntegrationTest

□ 3 个以上服务合作吗?
  → Yes: IntegrationTest

全部 No → 使用 MockingUnitTest

测试助手结构

IntegrationTest - 集成测试

@Tag("Integration")
@SpringBootTest
public abstract class IntegrationTest {
    // 完整 Spring Context, Testcontainers 利用
}

何时: 多个服务合作,需要实际 DB/外部系统 注意: 最重,外部 API 用 @MockBean 使用

RdbTest - Repository 测试

@Tag("RDB")
@DataJpaTest
public abstract class RdbTest {}

何时: Repository CRUD, QueryDSL 查询,N+1 问题验证

ControllerTest - API 测试

@Tag("Controller")
@WebMvcTest(TargetController.class)
public abstract class ControllerTest {
    @Autowired
    protected MockMvc mockMvc;
}

何时: API 端点,HTTP Status, 输入验证 注意: Service 用 @MockBean 必须处理

RedisTest - Redis 测试

@Tag("Redis")
@DataRedisTest
public abstract class RedisTest {}

何时: Redis 缓存,会话存储验证

MockingUnitTest - Service 单元测试

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;
}

何时: Service 逻辑单元测试,快速测试 注意: 没有 Spring Context, @Autowired 不可用

PojoUnitTest - 领域逻辑测试

class UserTest {
    @Test
    void activate_Success() {
        // 纯 Java 逻辑测试
    }
}

何时: 领域实体,VO,工具类


Fixture Monkey - 测试数据生成

依赖设置

// Gradle
testImplementation 'com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.13'
<!-- Maven -->
<dependency>
    <groupId>com.navercorp.fixturemonkey</groupId>
    <artifactId>fixture-monkey-starter</artifactId>
    <version>1.0.13</version>
    <scope>test</scope>
</dependency>

使用方法

import static {your.package}.fixture.FixtureFactory.FIXTURE_MONKEY;

// 简单生成
User user = FIXTURE_MONKEY.giveMeOne(User.class);

// 特定字段指定
User user = FIXTURE_MONKEY.giveMeBuilder(User.class)
    .set("email", "test@example.com")
    .set("active", true)
    .sample();

// 多个生成
List<User> users = FIXTURE_MONKEY.giveMe(User.class, 10);

Given-When-Then 模式(必须)

所有测试必须遵循 Given-When-Then 模式

@Test
@DisplayName("用户创建 - 成功")
void createUser_Success() {
    // given - 测试准备
    UserRequest request = new UserRequest("test@example.com", "password");
    User savedUser = FIXTURE_MONKEY.giveMeOne(User.class);
    when(userRepository.save(any())).thenReturn(savedUser);

    // when - 实际执行
    UserResponse response = userService.createUser(request);

    // then - 验证
    assertThat(response).isNotNull();
    verify(userRepository, times(1)).save(any());
}

AssertJ 验证模式

// 单个值
assertThat(response).isNotNull();
assertThat(response.userId()).isEqualTo(1L);

// 集合
assertThat(users).hasSize(3);
assertThat(users).extracting(User::getEmail)
    .containsExactlyInAnyOrder("a@test.com", "b@test.com");

// Boolean
assertThat(user.isActive()).isTrue();

// 异常
assertThatThrownBy(() -> userService.findById(999L))
    .isInstanceOf(BusinessException.class)
    .hasMessageContaining("USER_NOT_FOUND");

// Optional
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("홍길동");

Mockito 模式

Mock 设置

// 返回值
when(userRepository.findById(1L)).thenReturn(Optional.of(user));

// void 方法
doNothing().when(emailService).sendEmail(any());

// 异常发生
when(userRepository.findById(999L))
    .thenThrow(new BusinessException(ErrorCode.USER_NOT_FOUND));

Mock 调用验证

// 调用次数
verify(userRepository, times(1)).findById(1L);
verify(userRepository, never()).delete(any());

// 参数验证
verify(userRepository).save(argThat(user ->
    user.getEmail().equals("test@example.com")
));

测试命名规则

class ApplyServiceIntegrationTest extends IntegrationTest  // Integration
class UserRepositoryTest extends RdbTest                   // Repository
class UserControllerTest extends ControllerTest            // Controller
class UserServiceTest                                      // Service Unit
class UserTest                                             // Domain

方法

// 模式: {方法名}_{场景}_{预期结果}
@Test
@DisplayName("用户创建 - 成功")
void createUser_ValidRequest_Success()

@Test
@DisplayName("用户查询 - 用户不存在")
void findById_UserNotFound_ThrowsException()

测试示例

Controller 测试

@DisplayName("User -> UserController 测试")
@WebMvcTest(UserController.class)
class UserControllerTest extends ControllerTest {

    @MockBean
    private UserService userService;

    @Test
    @DisplayName("用户查询 API - 成功")
    void getUser_Success() throws Exception {
        // given
        Long userId = 1L;
        UserResponse response = new UserResponse(userId, "test@example.com");
        when(userService.findById(userId)).thenReturn(response);

        // when & then
        mockMvc.perform(get("/api/v1/users/{id}", userId))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.userId").value(userId));
    }
}

Service 单元测试

@ExtendWith(MockitoExtension.class)
@DisplayName("User -> UserService 单元测试")
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    @DisplayName("用户查询 - 成功")
    void findById_Success() {
        // given
        Long userId = 1L;
        User user = FIXTURE_MONKEY.giveMeBuilder(User.class)
            .set("id", userId)
            .sample();
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        // when
        UserResponse response = userService.findById(userId);

        // then
        assertThat(response).isNotNull();
        assertThat(response.userId()).isEqualTo(userId);
        verify(userRepository, times(1)).findById(userId);
    }
}

Repository 测试

@DisplayName("User -> UserRepository 测试")
class UserRepositoryTest extends RdbTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("活跃用户查询 - 成功")
    void findActiveUsers_Success() {
        // given
        User active = FIXTURE_MONKEY.giveMeBuilder(User.class)
            .set("active", true)
            .sample();
        userRepository.save(active);

        // when
        List<UserDto> result = userRepository.findActiveUsers();

        // then
        assertThat(result).hasSize(1);
        assertThat(result).extracting(UserDto::email)
            .contains(active.getEmail());
    }
}

何时使用此技能

此技能在以下情况下自动应用:

  • 测试文件创建或修改
  • 测试助手选择(IntegrationTest vs MockingUnitTest 判断)
  • 测试数据生成(Fixture Monkey 使用)
  • Given-When-Then 模式应用
  • AssertJ 验证代码编写
  • Mockito Mock 设置及验证

特别重要: 新 Service 测试编写时先确认 “Mock vs Integration 选择标准”!


检查列表

测试代码编写时确认事项:

所有测试共同

  • [ ] 遵循 Given-When-Then 模式?
  • [ ] 使用 @DisplayName 测试意图明确?
  • [ ] 使用 AssertJ 验证?
  • [ ] 方法名是 方法_场景_结果 模式?

测试助手选择(首先确认!)

  • [ ] 涉及金钱处理(存款/取款/支付)或事务回滚验证? → IntegrationTest
  • [ ] 需要确认 3 个以上表一致性或 DB 约束验证? → IntegrationTest
  • [ ] 需要验证复杂状态转移(3 阶段+)或事件发布/监听器验证? → IntegrationTest
  • [ ] 3 个以上服务合作? → IntegrationTest
  • [ ] 以上条件都不符合 → 使用 MockingUnitTest

IntegrationTest

  • [ ] 符合上述选择标准之一以上?
  • [ ] 外部 API 用 @MockBean 处理了?
  • [ ] 真的需要 IntegrationTest 再次确认?

RdbTest

  • [ ] 只包含 Repository/QueryDSL 测试?
  • [ ] 验证 N+1 问题?

ControllerTest

  • [ ] 明确 @WebMvcTest(TargetController.class)?
  • [ ] Service 用 @MockBean 处理了?
  • [ ] 验证 HTTP Status Code?

MockingUnitTest

  • [ ] 用 @Mock 依赖,@InjectMocks 测试对象注入了?
  • [ ] 用 verify() Mock 调用验证了?

PojoUnitTest

  • [ ] 只测试领域逻辑?
  • [ ] 没有外部依赖?

测试执行命令

Gradle

./gradlew test                                    # 全部测试
./gradlew test --tests * -Dtest.tags=Integration # 标签执行
./gradlew test --tests UserServiceTest            # 特定类

Maven

./mvnw test                        # 全部测试
./mvnw test -Dgroups=Integration   # 标签执行
./mvnw test -Dtest=UserServiceTest # 特定类

测试质量标准

  1. 覆盖率: 核心业务逻辑 70% 以上
  2. 隔离性: 每个测试可以独立执行
  3. 速度: 单元测试 1 秒内,集成测试 5 秒内
  4. 明确性: 仅凭测试名称就能理解意图
  5. 可靠性: 相同输入总是相同结果