SuperAPI AI Directory

junit-mockito-patterns

Data & analytics

View repository

Updated 10-16 08:47

About this skill

JUnit5 + Mockito testing rules

Source: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 migration rules — @ExtendWith replaces @RunWith, @Test imports org.junit.jupiter.api
  2. Mockito rules — @Mock (external dependencies) + @InjectMocks (object under test), do not Mock POJOs
  3. Test isolation (FIRST principles) — Fast/Independent/Repeatable/Self-Validating/Timely
  4. Slice tests — @WebMvcTest(Controller) / @DataJpaTest(Repository) / @JsonTest
  5. BDD style — BDDMockito.given()/willReturn()/then().should() replaces when/thenReturn/verify
  6. Behavior verification — verify() + ArgumentCaptor
  7. @ParameterizedTest + @MethodSource — parameterized tests simplify multiple scenarios
  8. @Nested grouping — organize related test scenarios

❌ Out of Scope

  1. Integration testing/end-to-end testing → Playwright/Selenium(existing skill)
  2. Performance testing → JMH(Java Microbenchmark Harness)

JUnit4 → JUnit5 migration comparison

JUnit4 JUnit5 Description
@RunWith(SpringRunner.class) @ExtendWith(SpringExtension.class) Extension mechanism
@RunWith(MockitoJUnitRunner.class) @ExtendWith(MockitoExtension.class) Mockito extension
@Test(expected = Exception.class) assertThrows(Exception.class, () -> {...}) Exception assertion
@Before @BeforeEach Executed before each test
@After @AfterEach Executed after each test
@BeforeClass @BeforeAll Before all tests (static)
@AfterClass @AfterAll After all tests (static)
@Ignore @Disabled Disable test
@FixMethodOrder @TestMethodOrder(MethodName) Test order

Most common mistakes made by LLMs

# Error Correct approach
1 import org.junit.Test (JUnit4) import org.junit.jupiter.api.Test (JUnit5)
2 @RunWith(MockitoJUnitRunner.class) @ExtendWith(MockitoExtension.class)
3 Testing a Service with @SpringBootTest starts the entire context Use @ExtendWith(MockitoExtension.class) pure Mockito
4 Controller testing starts the entire application @WebMvcTest + @MockBean loads only the Controller layer
5 Mock value objects (POJO/DTO) Only Mock external dependencies (DB/HTTP/MQ), directly new POJOs
6 verify(mock, never()) do not write times(0) verify(mock, never()).method() or verify(mock, times(0)).method()
7 Every test method has initMocks(this) @ExtendWith automatic initialization
8 Use when().thenReturn() (non-BDD) BDD style: given().willReturn()
9 @SpringBootTest for unit testing (slow, 3s+) Slice testing < 200ms
10 Do not write @DisplayName Chinese descriptions for test methods, making it easier to locate issues

Core rules quick reference

Pure unit testing (recommended)

@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 slice testing

@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 slice testing

@DataJpaTest
@DisplayName("用户仓库测试")
class UserRepositoryTest {
    @Autowired TestEntityManager em;
    @Autowired UserRepository repo;

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

    @Test
    @DisplayName("按用户名查询-不存在-返回空")
    void findByUsername_NotExists_ReturnEmpty() {
        Optional<User> result = repo.findByUsername("nonexistent");
        assertThat(result).isEmpty();
    }
}

Parameterized testing

@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 parameter capture

@Test
@DisplayName("注册用户-验证发送欢迎邮件")
void registerUser_ShouldSendWelcomeEmail() {
    @Captor ArgumentCaptor<EmailMessage> emailCaptor;
    given(mailClient.send(any())).willReturn(true);
    
    userService.register("test@test.com", "张三");
    
    then(mailClient).should().send(emailCaptor.capture());
    EmailMessage msg = emailCaptor.getValue();
    assertThat(msg.getTo()).isEqualTo("test@test.com");
    assertThat(msg.getSubject()).contains("欢迎");
}

@Nested organizing tests

@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() { ... }
    }
}

Test selection decision tree

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

Gotchas

  1. @Mock does not Mock POJOs — only Mock external dependencies, directly new POJOs
  2. @SpringBootTest starts the entire context (slow) — prefer slice tests @WebMvcTest/@DataJpaTest
  3. AssertJ assertions are better than assertEquals — assertThat(x).isEqualTo(y) more readable, chained calls
  4. @MockBean and @Mock are different — @MockBean is a Mock in the Spring container (integration testing), @Mock is pure Mockito (unit testing)
  5. verify() default verification count is times(1) — 0 calls require explicitly using never() or times(0)
  6. @MockitoExtension automatically initializes mocks — No need to call MockitoAnnotations.openMocks(this) in @BeforeEach
  7. Test method naming — methodName_StateUnderTest_ExpectedBehavior or @DisplayName Chinese description
  8. JUnit5 @Nested to organize related tests — replaces JUnit4's @FixMethodOrder
  9. @InjectMocks must be a concrete class — cannot be an interface or abstract class (needs instantiation)
  10. Mockito cannot mock constructors, final classes, or static methods (by default) — requires PowerMock/Mockito Inline
  11. BDDMockito.then(mock).should() replaces verify(mock) — better fits the Given-When-Then structure
  12. @Captor is more concise than manually creating an ArgumentCaptor — declarative argument capture

Data Privacy

This skill does not collect, store, or transmit any user data.