사실 테스트 코드 작성을 진짜 못한다. 매번 그냥 뷰를 구현해놓고 테스트를 진행한 적도 많고, 어떻게 진행해야할지 아니면 어떻게 해야 내가 의도한대로 테스트를 할 수 있을지 막막했다. 따라서 간단하게 정리해볼려고 한다.
단위 테스트는 하나의 모듈(또는 함수, 메서드 등)을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트
특정 기능이나 로직이 정해진 입력에 대해 올바른 결과를 반환하는지를 개별적으로 확인
테스트 대상이 되는 유닛(Unit)은 다른 부분과 분리된 상태에서 테스트되므로, 외부 의존성(DB, 네트워크 등)은 Mocking 등의 방법으로 대체하는 것이 일반적이다.
🤔 하나의 모듈????
쉽게 말해서Calculator클래스가 존재한다면, 그 안의add()함수처럼 독립적으로 테스트할 수 있는 작은 기능 하나를 말한다. 이걸 기준으로 단위 테스트를 진행한다.
여러 컴포넌트들이 서로 상호작용하는 상황을 테스트하는 방식
모든 컴포넌트가 실제로 구동된 상태에서 테스트를 수행
예를 들어, DB, 캐시, 외부 API 등을 사용하는 경우, 실제 연결이 필요
그만큼 설정이 복잡하고, 테스트 시간이 오래 걸린다.
하나의 함수나 메서드처럼 작고 독립적인 단위의 기능을 테스트
외부 환경(DB, 캐시 등)에 의존하지 않도록 Mocking 등을 사용해 격리
속도가 빠르고, 개발 중 자주 실행하면서 피드백을 받을 수 있다.
보통은 JUnit과 AssertJ 를 많이 사용한다.
@Test
void testAdd() {
// Given
Calculator calc = new Calculator();
// When
int result = calc.add(2, 3);
// Then
assertThat(result).isEqualTo(5);
}
Java에서 가장 많이 사용되는 단위 테스트 프레임워크
| 애너테이션 | 설명 |
|---|---|
@Test | 테스트 메서드임을 나타냄 |
@DisplayName("설명") | 테스트 이름을 읽기 쉽게 표현 |
@BeforeEach | 각 테스트 시작 전 실행될 메서드 |
@AfterEach | 각 테스트 종료 후 실행될 메서드 |
@BeforeAll | 전체 테스트 시작 전에 딱 한 번 실행 (static) |
@AfterAll | 전체 테스트 끝난 후 딱 한 번 실행 (static) |
@Disabled | 해당 테스트를 일시적으로 실행하지 않음 |
@BeforeEach
void setUp() {
System.out.println("테스트 전에 실행됨");
}
@AfterEach
void tearDown() {
System.out.println("테스트 후에 실행됨");
}
@Test
@DisplayName("더하기 기능 테스트")
void testAdd() {
Calculator calc = new Calculator();
int result = calc.add(2, 3);
assertEquals(5, result);
}
JUnit의 단점인 읽기 어려운 assertion 문법을 보완해주는 라이브러리
즉, 테스트 결과를 더 직관적이고 가독성 있게 검증할 수 있게 도와준다.
| 구분 | JUnit Assertions (기본) | AssertJ assertThat() (추천) | 설명 |
|---|---|---|---|
| 기본 값 비교 | assertEquals(expected, actual) | assertThat(actual).isEqualTo(expected) | 값 비교 시 assertThat이 더 자연스럽고 가독성 좋음 |
| 부등호 비교 | 없음 (조건문 사용) | .isGreaterThan(), .isLessThan() 등 제공 | 숫자 크기 비교에 특화된 메서드 지원 |
| 논리 조건 | assertTrue(condition), assertFalse(condition) | .isTrue(), .isFalse() | 체이닝과 자연어 느낌의 문법 |
| null 체크 | assertNull(actual), assertNotNull(actual) | .isNull(), .isNotNull() | 간결하고 직관적 |
| 배열/컬렉션 | assertArrayEquals(expected, actual) | .contains(), .hasSize(), .isEmpty() 등 다양 | 컬렉션 검증에 훨씬 풍부하고 편리 |
| 예외 처리 | assertThrows(Exception.class, () -> {...}) | assertThatThrownBy(() -> {...}).isInstanceOf(Exception.class) | 예외 발생 및 메시지 검증에 더 상세 |
| 객체 동일성 | assertSame(expected, actual) | .isSameAs(), .isNotSameAs() | 인스턴스 동일성 체크 |
| 복합 검증 | assertAll(() -> ..., () -> ...) | 여러 assertThat() 체이닝 또는 SoftAssertions 지원 | 다중 검증 시 가독성 우수 |
| 테스트 실행 관리 | @Test, @BeforeEach, @AfterEach 등 | 동일 | JUnit 기반으로 같이 사용 |

Mockito는 개발자가 테스트 중에 동작을 직접 제어할 수 있도록 가짜 객체(Mock)를 제공하는 테스트 프레임워크다. 주로 의존성이 있는 컴포넌트나 외부 시스템(예: 데이터베이스, 네트워크 요청 등)을 직접 호출하지 않고도 테스트를 수행할 수 있게 해주므로, 테스트의 독립성, 속도, 안정성을 크게 향상시킨다.
일반적으로 스프링에서 개발을 하다 보면 여러 객체들 간의 의존성이 생기게 되는데, 이는 서비스 클래스가 리포지토리나 외부 API 클라이언트 등 다른 컴포넌트에 의존하면서 발생한다. 이러한 의존성은 실제 객체를 사용하는 경우 테스트 환경 구성에 많은 비용과 시간이 들게 만들며, 예측 불가능한 외부 요소로 인해 테스트 결과가 불안정해질 수 있다. 이때 Mockito를 활용하면 의존 객체를 가짜(Mock)로 대체하여 테스트 대상 클래스만을 분리해 빠르고 안정적으로 검증이 가능하다.
가짜(Mock) 생성과 관련된 어노테이션은
@Mock,@Spy,@InjectMock이 존재한다.
@Mock 애노테이션으로 생성된 객체는 실제 구현이 아닌 동작을 정의하지 않은 가짜(Mock) 객체이기 때문에, 그 내부의 메서드를 호출해 원하는 값을 얻으려면 반드시 스터빙(stubbing) 작업이 필요하다.
특정 메서드가 호출되었을 때 어떤 값을 반환할지 또는 어떤 동작을 수행할지를 미리 정의해두는 것을 말한다.
| 메서드 명 | 설명 |
|---|---|
when(...) | 어떤 메서드 호출에 대해 동작을 정의할지 지정 |
thenReturn(...) | 지정한 메서드가 호출되었을 때 어떤 값을 반환할지 설정 |
thenThrow(...) | 지정한 메서드가 호출되었을 때 어떤 예외를 발생시킬지 설정 |
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "Alice")));
when(orderService.placeOrder(any()))
.thenThrow(new IllegalStateException("중복 주문"));
📌 any()란?
when(...).thenReturn(...) 또는 verify(...) 등에서
특정 타입의 어떤 값이든 허용하겠다는 의미로 사용된다.
예를 들어any(String.class)는 어떤 문자열이 와도 상관없다는 뜻
// 테스트 클래스
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository; // 가짜 객체 생성
@Test
void testGetUsernameById() {
// 스터빙: findById(1L)가 호출되면 User("Alice")를 반환하도록 설정
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "Alice")));
UserService userService = new UserService(userRepository);
String result = userService.getUsernameById(1L);
assertEquals("Alice", result); // 테스트 성공
}
}
@InjectMocks는 Mockito에서 의존성 주입(DI)을 자동으로 처리해주는 애노테이션이다. 테스트 대상 클래스에 붙이면, 해당 클래스의 필드에 @Mock 또는 @Spy로 생성된 가짜 객체들을 자동으로 주입(inject)해 준다. 이 덕분에 개발자는 별도로 생성자나 세터로 주입하지 않아도 간편하게 테스트를 구성할 수 있다.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository; // 가짜 객체 생성
@InjectMocks
private UserService userService; // @Mock 객체 자동 주입
@Test
void testGetUsernameById() {
// 스터빙
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "Alice")));
String result = userService.getUsernameById(1L);
assertEquals("Alice", result);
}
}
🔧 리팩토링 전 (직접 주입)
UserService userService = new UserService(userRepository);
🔧 리팩토링 후 (Mockito가 자동 주입)
@InjectMocks
private UserService userService;
검증(Verify)은 “스터빙(stubbing)으로 설정한 메서드가 실제로 호출되었는지”, “원하는 횟수만큼 호출되었는지” 등을 확인하는 기능
verify(userRepository, times(1)).findById(1L);
| 메서드 명 | 설명 |
|---|---|
times(n) | 정확히 n번 호출됐는지 검증 |
never() | 한 번도 호출되지 않았는지 검증 (times(0)과 동일) |
atLeastOnce() | 최소 한 번 이상 호출됐는지 검증 (atLeast(1)과 동일) |
atLeast(n) | 최소 n번 이상 호출됐는지 검증 |
atMostOnce() | 최대 한 번 이하로 호출됐는지 검증 (atMost(1)과 동일) |
atMost(n) | 최대 n번 이하로 호출됐는지 검증 |
calls(n) | 정확히 n번 호출됐는지 검증 (times(n)의 별칭, Mockito 5.x부터) |
timeout(long ms) | 지정한 ms 이내에 호출되지 않으면 실패 (비동기 검증에 유용) |
after(long ms) | 지정한 ms 이후에 검증을 수행. 즉시 실패하지 않고 대기 |
description(...) | 검증 실패 시 출력될 커스텀 메시지를 지정 |
사진 참고 :