mcpField
J
junit-mockito-patterns

junit-mockito-patterns

github.com/full-stack-skillsテスト・品質796 回インストール4 週間前に更新
リポジトリ

junit-mockito-patterns について

junit-mockito-patterns は、JUnit5 と Mockito で Java テストを書く開発者向けのスキルです。移行方法やテスト設計パターンに加え、単体テストと Spring のスライステストの選び方を案内します。

主な機能

  1. 01JUnit4 から JUnit5 への移行案内
  2. 02Mockito のモックと注入パターン
  3. 03FIRST に基づくテスト分離
  4. 04Spring MVC と JPA のスライステスト
  5. 05BDD 形式の振る舞い検証
  6. 06パラメーター化・ネストテスト

活用シーン

  1. 01既存の JUnit4 テストの移行
  2. 02依存をモックしたサービスのテスト
  3. 03スライステストによるコントローラーやリポジトリーの検証
  4. 04パラメーター化テストで複数ケースを網羅

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/full-stack-skills/java-skills --skill junit-mockito-patterns

インストール後、Claude Code で/junit-mockito-patternsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
examples/
junit-realworld.md
references/
junit-resources.md

詳細

プロジェクトの README より

JUnit5 + Mockito 测试规则

来源:https://junit.org/junit5/docs/current/user-guide/
Mockito:https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html

Capability Boundaries

✅ Strong Suits
  1. JUnit5 迁移规则 — @ExtendWith 替代 @RunWith, @Test 导入 org.junit.jupiter.api
  2. Mockito 规则 — @Mock(外部依赖) + @InjectMocks(被测试对象), 不 Mock POJO
  3. 测试隔离(FIRST原则) — Fast/Independent/Repeatable/Self-Validating/Timely
  4. 切片测试 — @WebMvcTest(Controller) / @DataJpaTest(Repository) / @JsonTest
  5. BDD风格 — BDDMockito.given()/willReturn()/then().should() 替代 when/thenReturn/verify
  6. 行为验证 — verify() + ArgumentCaptor
  7. @ParameterizedTest + @MethodSource — 参数化测试简化多场景
  8. @Nested 分组 — 组织相关测试场景
❌ Out of Scope
  1. 集成测试/端到端测试 → Playwright/Selenium(已有技能)
  2. 性能测试 → JMH(Java Microbenchmark Harness)

JUnit4 → JUnit5 迁移对照

JUnit4JUnit5说明
@RunWith(SpringRunner.class)@ExtendWith(SpringExtension.class)扩展机制
@RunWith(MockitoJUnitRunner.class)@ExtendWith(MockitoExtension.class)Mockito扩展
@Test(expected = Exception.class)assertThrows(Exception.class, () -> {...})异常断言
@Before@BeforeEach每个测试前执行
@After@AfterEach每个测试后执行
@BeforeClass@BeforeAll所有测试前(static)
@AfterClass@AfterAll所有测试后(static)
@Ignore@Disabled禁用测试
@FixMethodOrder@TestMethodOrder(MethodName)测试顺序

LLM最常犯的错误

#错误正确做法
1import org.junit.Test (JUnit4)import org.junit.jupiter.api.Test (JUnit5)
2@RunWith(MockitoJUnitRunner.class)@ExtendWith(MockitoExtension.class)
3测试 Service 用 @SpringBootTest 启动整个上下文用 @ExtendWith(MockitoExtension.class) 纯 Mockito
4Controller 测试启动整个应用@WebMvcTest + @MockBean 只加载 Controller 层
5Mock 值对象(POJO/DTO)只 Mock 外部依赖(DB/HTTP/MQ),POJO 直接 new
6verify(mock, never()) 不写 times(0)verify(mock, never()).method() 或 verify(mock, times(0)).method()
7每个测试方法都 initMocks(this)@ExtendWith 自动初始化
8用 when().thenReturn() (非BDD)BDD 风格: given().willReturn()
9@SpringBootTest 做单元测试(慢,3s+)切片测试 < 200ms
10不写 @DisplayName测试方法中文描述,方便定位

核心规则速查

纯单元测试(推荐)
@ExtendWith(MockitoExtension.class)
@DisplayName("用户服务测试")
class UserServiceTest {
    @Mock UserRepository userRepo;     // 外部依赖
    @Mock CacheManager cacheManager;
    @InjectMocks UserService userService; // 被测试对象(构造器注入)

    @Test
    @DisplayName("查询用户-缓存命中-直接返回")
    void getUser_CacheHit_ReturnFromCache() {
        // given(BDD风格)
        User cached = new User(1L, "zhang");
        given(cacheManager.get("user:1")).willReturn(cached);
        // when
        User result = userService.getUser(1L);
        // then
        assertThat(result.getName()).isEqualTo("zhang");
        then(userRepo).should(never()).findById(anyLong());  // BDD验证
    }

    @Test
    @DisplayName("查询用户-未命中-查DB并缓存")
    void getUser_CacheMiss_QueryDBAndCache() {
        // given
        given(cacheManager.get("user:1")).willReturn(null);
        given(userRepo.findById(1L)).willReturn(Optional.of(new User(1L, "zhang")));
        // when
        User result = userService.getUser(1L);
        // then
        assertThat(result).isNotNull();
        then(cacheManager).should().set(eq("user:1"), any(User.class));
    }
}
Controller 切片测试
@WebMvcTest(UserController.class)
@DisplayName("用户控制器测试")
class UserControllerTest {
    @Autowired MockMvc mockMvc;
    @MockBean UserService userService;

    @Test
    @DisplayName("GET /user/1 返回用户信息")
    void getUser_ById_ReturnsUser() throws Exception {
        given(userService.getUser(1L)).willReturn(new UserVO(1L, "zhang"));
        mockMvc.perform(get("/user/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.username").value("zhang"));
    }

    @Test
    @DisplayName("GET /user/1 用户不存在返回404")
    void getUser_NotFound_Returns404() throws Exception {
        given(userService.getUser(99L)).willThrow(new UserNotFoundException(99L));
        mockMvc.perform(get("/user/99"))
            .andExpect(status().isNotFound());
    }
}
Repository 切片测试
@DataJpaTest
@DisplayName("用户仓库测试")
class UserRepositoryTest {
    @Autowired TestEntityManager em;
    @Autowired UserRepository repo;

    @Test
    @DisplayName("按用户名查询-存在-返回用户")
    void findByUsername_Exists_ReturnUser() {
        em.persistAndFlush(new User("zhang", "[email protected]"));
        Optional<User> result = repo.findByUsername("zhang");
        assertThat(result).isPresent();
        assertThat(result.get().getEmail()).isEqualTo("[email protected]");
    }

    @Test
    @DisplayName("按用户名查询-不存在-返回空")
    void findByUsername_NotExists_ReturnEmpty() {
        Optional<User> result = repo.findByUsername("nonexistent");
        assertThat(result).isEmpty();
    }
}
参数化测试
@ExtendWith(MockitoExtension.class)
class PriceServiceTest {
    @InjectMocks PriceService priceService;

    @ParameterizedTest
    @MethodSource("discountParams")
    @DisplayName("计算折扣-多场景验证")
    void calcDiscount_Scenarios(BigDecimal price, int level, BigDecimal expected) {
        assertThat(priceService.calcDiscount(price, level))
            .isEqualByComparingTo(expected);
    }

    static Stream<Arguments> discountParams() {
        return Stream.of(
            Arguments.of(new BigDecimal("100"), 1, new BigDecimal("100")),
            Arguments.of(new BigDecimal("100"), 2, new BigDecimal("90")),  // 9折
            Arguments.of(new BigDecimal("100"), 3, new BigDecimal("80"))   // 8折
        );
    }
}
ArgumentCaptor 参数捕获
@Test
@DisplayName("注册用户-验证发送欢迎邮件")
void registerUser_ShouldSendWelcomeEmail() {
    @Captor ArgumentCaptor<EmailMessage> emailCaptor;
    given(mailClient.send(any())).willReturn(true);
    
    userService.register("[email protected]", "张三");
    
    then(mailClient).should().send(emailCaptor.capture());
    EmailMessage msg = emailCaptor.getValue();
    assertThat(msg.getTo()).isEqualTo("[email protected]");
    assertThat(msg.getSubject()).contains("欢迎");
}
@Nested 组织测试
@ExtendWith(MockitoExtension.class)
@DisplayName("订单服务测试")
class OrderServiceTest {
    @Mock OrderRepository orderRepo;
    @InjectMocks OrderService orderService;

    @Nested
    @DisplayName("创建订单")
    class CreateOrder {
        @Test
        @DisplayName("正常创建-返回订单ID")
        void normal_ReturnOrderId() { ... }

        @Test
        @DisplayName("库存不足-抛异常")
        void noStock_ThrowException() { ... }
    }

    @Nested
    @DisplayName("取消订单")
    class CancelOrder {
        @Test
        @DisplayName("已支付-退款并取消")
        void paid_RefundAndCancel() { ... }

        @Test
        @DisplayName("已取消-幂等")
        void alreadyCancelled_Idempotent() { ... }
    }
}

测试选择决策树

需要测试什么?
├── Service/业务逻辑
│   ├── 纯逻辑(无外部依赖) → @Test + new 对象
│   └── 有DB/HTTP/MQ依赖 → @ExtendWith(MockitoExtension.class) + @Mock/@InjectMocks
├── Controller/API
│   └── @WebMvcTest + @MockBean
├── Repository/数据层
│   └── @DataJpaTest + TestEntityManager
├── JSON序列化
│   └── @JsonTest
└── 集成测试(多组件)
    └── @SpringBootTest(慎用,仅关键流程)

Gotchas

  1. @Mock 不 Mock POJO — 只 Mock 外部依赖,POJO 直接 new
  2. @SpringBootTest 启动整个上下文(慢) — 优先用切片测试 @WebMvcTest/@DataJpaTest
  3. AssertJ 断言优于 assertEquals — assertThat(x).isEqualTo(y) 更可读、链式调用
  4. @MockBean 和 @Mock 不同 — @MockBean 是 Spring 容器中的 Mock(集成测试),@Mock 是纯 Mockito(单元测试)
  5. verify() 验证次数默认 times(1) — 调用0次需显式用 never() 或 times(0)
  6. @MockitoExtension 自动初始化 mocks — 不需要在 @BeforeEach 调用 MockitoAnnotations.openMocks(this)
  7. 测试方法命名 — methodName_StateUnderTest_ExpectedBehavior 或 @DisplayName 中文描述
  8. JUnit5 @Nested 组织相关测试 — 替代 JUnit4 的 @FixMethodOrder
  9. @InjectMocks 必须是具体类 — 不能是接口或抽象类(需要实例化)
  10. Mockito 不能 Mock 构造方法、final 类、静态方法(默认) — 需要 PowerMock/Mockito Inline
  11. BDDMockito.then(mock).should() 替代 verify(mock) — 更符合 Given-When-Then 结构
  12. @Captor 比手动创建 ArgumentCaptor 更简洁 — 声明式参数捕获

Data Privacy

本技能不收集、存储或传输任何用户数据。

コメント