
테스트를 어디서부터 작성하느냐에 따라 접근 방식은 크게 두 가지로
나뉩니다.
테스트 전략을 설명할 때 자주 사용하는 개념이 Test Pyramid 입니다.

요구사항:
/api/login구조:
Controller → Service → Repository
사용자 관점(API)부터 테스트를 작성합니다.
@RestController
@RequestMapping("/api")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest req) {
String token = authService.login(req.email(), req.password());
return ResponseEntity.ok(new LoginResponse(token));
}
}
public record LoginRequest(String email, String password) {}
public record LoginResponse(String token) {}
@WebMvcTest(AuthController.class)
class AuthControllerTest {
@Autowired MockMvc mockMvc;
@MockBean AuthService authService;
@Test
void login_success_returnsToken() throws Exception {
given(authService.login("a@b.com", "pw")).willReturn("token-123");
mockMvc.perform(post("/api/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"a@b.com\",\"password\":\"pw\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").value("token-123"));
}
}
✔ API 계약을 먼저 고정\
✔ 내부 구현은 Mock 처리
핵심 비즈니스 로직부터 테스트합니다.
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordHasher passwordHasher;
private final TokenIssuer tokenIssuer;
public AuthService(UserRepository userRepository,
PasswordHasher passwordHasher,
TokenIssuer tokenIssuer) {
this.userRepository = userRepository;
this.passwordHasher = passwordHasher;
this.tokenIssuer = tokenIssuer;
}
public String login(String email, String password) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new BadCredentialsException("bad"));
if (!passwordHasher.matches(password, user.passwordHash())) {
throw new BadCredentialsException("bad");
}
return tokenIssuer.issue(user.id());
}
}
@ExtendWith(MockitoExtension.class)
class AuthServiceTest {
@Mock UserRepository userRepository;
@Mock PasswordHasher passwordHasher;
@Mock TokenIssuer tokenIssuer;
@InjectMocks AuthService authService;
@Test
void login_success_issuesToken() {
User user = new User(1L, "a@b.com", "HASH");
given(userRepository.findByEmail("a@b.com"))
.willReturn(Optional.of(user));
given(passwordHasher.matches("pw", "HASH"))
.willReturn(true);
given(tokenIssuer.issue(1L))
.willReturn("token-123");
String token = authService.login("a@b.com", "pw");
assertThat(token).isEqualTo("token-123");
}
}
✔ 도메인 규칙을 촘촘히 검증\
✔ 복잡한 정책이 늘어날수록 강력함
Outside-in / Inside-out은 헥사고날 구조와도 잘 맞습니다.
👉 결론: 둘 중 하나만 고집하지 말고, 상황에 맞게 섞어 쓰는 것이 가장
현실적입니다.