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
- JUnit5 migration rules — @ExtendWith replaces @RunWith, @Test imports org.junit.jupiter.api
- Mockito rules — @Mock (external dependencies) + @InjectMocks (object under test), do not Mock POJOs
- Test isolation (FIRST principles) — Fast/Independent/Repeatable/Self-Validating/Timely
- Slice tests — @WebMvcTest(Controller) / @DataJpaTest(Repository) / @JsonTest
- BDD style — BDDMockito.given()/willReturn()/then().should() replaces when/thenReturn/verify
- Behavior verification — verify() + ArgumentCaptor
- @ParameterizedTest + @MethodSource — parameterized tests simplify multiple scenarios
- @Nested grouping — organize related test scenarios
❌ Out of Scope
- Integration testing/end-to-end testing → Playwright/Selenium(existing skill)
- 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
- @Mock does not Mock POJOs — only Mock external dependencies, directly new POJOs
- @SpringBootTest starts the entire context (slow) — prefer slice tests @WebMvcTest/@DataJpaTest
- AssertJ assertions are better than assertEquals —
assertThat(x).isEqualTo(y)more readable, chained calls - @MockBean and @Mock are different — @MockBean is a Mock in the Spring container (integration testing), @Mock is pure Mockito (unit testing)
- verify() default verification count is times(1) — 0 calls require explicitly using
never()ortimes(0) - @MockitoExtension automatically initializes mocks — No need to call MockitoAnnotations.openMocks(this) in @BeforeEach
- Test method naming —
methodName_StateUnderTest_ExpectedBehavioror@DisplayNameChinese description - JUnit5 @Nested to organize related tests — replaces JUnit4's @FixMethodOrder
- @InjectMocks must be a concrete class — cannot be an interface or abstract class (needs instantiation)
- Mockito cannot mock constructors, final classes, or static methods (by default) — requires PowerMock/Mockito Inline
- BDDMockito.then(mock).should() replaces verify(mock) — better fits the Given-When-Then structure
- @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.