JUnit은 자바에서 널리 사용되는 단위 테스트 프레임워크로, 테스트 코드를 작성하고 실행하는 데 매우 유용합니다.
JUnit을 사용하면 개별 메서드나 클래스가 의도한 대로 동작하는지 검증할 수 있습니다.
JUnit 4와 JUnit 5가 많이 사용되며, JUnit 5는 JUnit 4에 비해 더 많은 기능과 유연성을 제공합니다.
@Test
void testMethod() {
// 테스트 코드
}
@BeforeEach
void setUp() {
// 초기화 코드
}
@AfterEach
void tearDown() {
// 정리 코드
}
@BeforeAll
static void init() {
// 전체 테스트에 앞서 한 번 실행될 코드
}
@AfterAll
static void cleanUp() {
// 전체 테스트가 끝난 후 한 번 실행될 코드
}
@Test
@DisplayName("1 + 1은 2가 되어야 한다")
void testAddition() {
assertEquals(2, 1 + 1);
}
@Disabled: 특정 테스트를 비활성화할 때 사용됩니다. 테스트는 작성되어 있지만, 실행하고 싶지 않을 때 이 어노테이션을 사용합니다.
@Test
@Disabled("아직 구현되지 않음")
void testDisabled() {
// 이 테스트는 실행되지 않음
}
- assertEquals(expected, actual): 예상 값과 실제 값이 같은지 확인합니다.
- assertTrue(condition): 조건이 참인지 확인합니다.
- assertFalse(condition): 조건이 거짓인지 확인합니다.
- assertNotNull(object): 객체가 null이 아닌지 확인합니다.
- assertThrows(expectedType, executable): 예외가 발생하는지 확인합니다.
예시:
@Test
void testAssertions() {
assertEquals(5, 2 + 3); // 두 값이 같은지 확인
assertTrue(5 > 3); // 조건이 참인지 확인
assertNotNull(new Object()); // 객체가 null이 아닌지 확인
}
테스트 클래스의 구성 테스트 클래스는 여러 테스트 메서드를 포함할 수 있습니다. 일반적으로 각 테스트 메서드는 독립적이어야 하며, 특정 순서에 의존하지 않도록 설계하는 것이 좋습니다. 예를 들어, 데이터베이스와 연동되는 테스트라면 테스트 환경을 초기화하고 종료할 때 데이터베이스를 리셋하는 작업이 필요할 수 있습니다.
Mocking과 Stubbing 테스트할 때 실제 의존성을 사용하는 대신, 가짜 객체(mock)를 사용할 수 있습니다. 이를 통해 의존성을 격리하고 특정 조건에서 코드를 테스트할 수 있습니다. 이를 위해 Mockito와 같은 라이브러리가 많이 사용됩니다.
@Test
void testWithMock() {
List<String> mockList = mock(List.class);
when(mockList.size()).thenReturn(5);
assertEquals(5, mockList.size());
}
이와 같은 원칙을 지키면 JUnit을 통해 효율적으로 테스트 코드를 작성할 수 있습니다.
assertThrows는 특정 메서드가 예외를 발생시키는지 확인할 때 사용합니다. 이 메서드를 사용할 때 주의할 점은 아래와 같습니다:
@Test
void testException() {
assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("예외 발생");
});
}
assertThrows(IllegalArgumentException.class, () -> someMethod("invalid input"));
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("Invalid argument");
});
assertEquals("Invalid argument", thrown.getMessage());
Mockito는 주로 의존성이 있는 클래스나 객체를 테스트할 때 유용합니다. 다음과 같은 상황에서 사용할 수 있습니다:
@Test
void testServiceWithMock() {
UserRepository mockRepo = mock(UserRepository.class);
when(mockRepo.findById(1)).thenReturn(new User(1, "John"));
UserService userService = new UserService(mockRepo);
User user = userService.getUserById(1);
assertEquals("John", user.getName());
}
복잡한 비즈니스 로직 테스트: 서비스 레이어나 비즈니스 로직을 테스트할 때 여러 객체 간의 상호작용을 목 객체로 처리하여 해당 로직만 집중적으로 테스트할 수 있습니다. 예를 들어, 서비스에서 여러 리포지토리나 API를 호출하는 경우, 각 의존성을 Mockito로 모킹하여 독립적인 테스트를 작성할 수 있습니다.
성능 및 상태 제어: 목 객체를 이용하면 테스트를 빠르고 효율적으로 수행할 수 있습니다. 예를 들어, 실제 파일을 읽고 쓰는 동작을 모킹하여 테스트 시간과 성능을 향상시킬 수 있습니다.
예외 상황 테스트: 목 객체는 특정 조건에서 예외를 던지도록 설정할 수 있기 때문에, 예외 상황도 쉽게 테스트할 수 있습니다. 이를 통해 에러 처리 로직을 검증할 수 있습니다.
when(mockRepo.findById(anyInt())).thenThrow(new IllegalArgumentException("Invalid ID"));
테스트 메서드들이 독립적이어야 하는 이유는 아래와 같습니다:
테스트의 신뢰성: 테스트가 독립적이지 않으면, 한 테스트의 결과가 다른 테스트에 영향을 미칠 수 있습니다. 예를 들어, 어떤 테스트가 데이터를 수정한 후 그 상태가 다음 테스트에 영향을 준다면, 테스트 실패 원인을 파악하기 어렵습니다. 각 테스트는 독립적으로 실행될 수 있어야 신뢰성 높은 테스트가 가능합니다.
테스트 유지 보수: 독립적인 테스트는 유지 보수가 쉽습니다. 테스트 메서드들이 서로 의존하면, 한 메서드의 변경이 다른 테스트에도 영향을 미칠 수 있습니다. 이는 테스트 유지 보수를 어렵게 만들고, 작은 코드 변경이 여러 테스트 실패를 유발할 수 있습니다.
병렬 실행 가능성: 테스트가 독립적일 경우, 여러 테스트를 병렬로 실행할 수 있습니다. 이는 전체 테스트 시간을 단축시키고, CI(지속적 통합) 환경에서 성능 향상에 기여합니다.
재현 가능성: 독립적인 테스트는 어떤 순서로 실행되든 항상 동일한 결과를 보장합니다. 의존성이 있는 테스트는 실행 순서에 따라 결과가 달라질 수 있으며, 이는 재현하기 어려운 버그를 초래할 수 있습니다.