Java 애플리케이션의 단위 테스트에서 널리 사용되는 JUnit은 개발자에게 필수적인 도구입니다. 🛠️
저는 평소 JUnit5를 사용해 왔지만, 최근에 JUnit4를 사용해 볼 기회가 생겼습니다.
처음에는 익숙하지 않은 버전이라 조금 낯설었지만, 이를 통해 두 버전의 차이를 직접 체감하며 JUnit의 발전을 이해할 수 있었습니다.
이 글에서는 JUnit4와 JUnit5의 차이점, 장단점, 그리고 제가 사용하면서 느낀 점을 정리해 보았습니다.👩🏻💻
JUnit4의 특징
JUnit4는 간단하고 직관적인 테스트 프레임워크로, 다음과 같은 어노테이션을 제공합니다.
- @Test: 테스트 메서드 표시.
- @Before / @After: 테스트 전/후에 실행되는 메서드 지정.
- @Ignore: 특정 테스트를 비활성화.
- @RunWith: 테스트 실행을 커스터마이징 하기 위한 기능.
장점
- 직관적인 사용법
- 간단한 프로젝트에서는 빠르게 적용 가능
단점
- 확장성 부족: 확장을 위해 복잡한 커스텀 Runner를 만들어야 함
- 가독성 부족: 테스트 이름을 명확히 지정할 수 있는 @DisplayName 기능이 없음
- 조건부 테스트 지원 부족: 직접 조건문을 작성하거나 Assume API를 사용해야 함
@RunWith(MockitoJUnitRunner.class)
public class ExampleTest {
@Before
public void setup() {
// 테스트 준비 코드
}
@Test
public void testExample() {
assertEquals(2, 1 + 1);
}
}
JUnit5의 특징
JUnit5는 JUnit4의 한계를 개선하고, 더 많은 기능과 확장성을 제공합니다.
모듈화
JUnit5는 3개의 모듈로 구성되어 있습니다:
- Jupiter: JUnit5의 핵심 API.
- Vintage: JUnit4와 호환성 유지.
- Platform: 테스트 실행 환경.
새로운 어노테이션
- @BeforeEach / @AfterEach: JUnit4의 @Before / @After 대체.
- @DisplayName: 테스트 이름을 명시적으로 지정.
- @Nested: 중첩된 테스트 클래스 지원.
확장성
- @ExtendWith를 통해 다양한 확장 기능을 손쉽게 추가.
Java 8 이상
- 람다 표현식과 스트림 API를 활용하여 간결한 테스트 작성 가능.
@ExtendWith(MockitoExtension.class)
class ExampleTest {
@BeforeEach
void setup() {
// 테스트 준비 코드
}
@Test
@DisplayName("간단한 덧셈 테스트")
void testExample() {
assertEquals(2, 1 + 1);
}
}
JUnit4와 JUnit5의 주요 차이점 ⚖️
기능 | JUnit4 | JUnit5 |
초기화 어노테이션 | @Before / @After | @BeforeEach / @AfterEach |
비활성화 어노테이션 | @Ignore | @Disabled |
테스트 이름 지정 | N/A | @DisplayName |
확장성 | @RunWith | @ExtendWith |
조건부 테스트 지원 | Assume API | @EnabledOnOs, @EnabledIf 등 |
병렬 실행 지원 | 미지원 | 지원 |
테스트 실행 환경 | 단일 Runner | 다중 환경 기능 |
조건부 테스트
JUnit5: @EnabledOnOs
JUnit5는 다양한 조건부 어노테이션을 제공하여 간단히 조건부 테스트를 작성할 수 있습니다.
@Test
@EnabledOnOs(OS.WINDOWS)
void testOnlyOnWindows() {
System.out.println("This test runs only on Windows");
}
@Test
@EnabledOnOs(OS.LINUX)
void testOnlyOnLinux() {
System.out.println("This test runs only on Linux");
}
JUnit4: Assume API
JUnit4에서는 조건부 테스트를 작성하기 위해 Assume API를 사용해야 합니다.
import org.junit.Test;
import org.junit.Assume;
public class ConditionalTestExample {
@Test
public void testOnlyOnWindows() {
String osName = System.getProperty("os.name").toLowerCase();
Assume.assumeTrue(osName.contains("win"));
System.out.println("This test runs only on Windows");
}
@Test
public void testOnlyOnLinux() {
String osName = System.getProperty("os.name").toLowerCase();
Assume.assumeTrue(osName.contains("linux"));
System.out.println("This test runs only on Linux");
}
}
JUnit5의 조건부 테스트는 @EnabledOnOs, @EnabledIf 같은 어노테이션을 통해 특정 조건에서 테스트를 실행할 수 있습니다.
이는 복잡한 조건문을 직접 작성하지 않아도 되므로, 테스트 코드의 가독성을 높이고 유지보수를 더 쉽게 만들어줍니다.
JUnit4 사용 경험에서 느낀 아쉬움과 배움
확장성의 제한
왜 JUnit4는 확장성이 제한되었다고 할까⁉️
JUnit4에서는 확장을 적용하려면 커스텀 Runner를 만들어야 합니다. 하나의 테스트 클래스는 하나의 Runner만 사용할 수 있기 때문에 특정 요구사항을 처리하려면 Runner를 직접 작성해야 합니다.
예를 들어, 아래는 JUnit4에서 특정 설정을 추가하려고 만든 커스텀 Runner의 코드입니다.
public class CustomRunner extends BlockJUnit4ClassRunner {
public CustomRunner(Class<?> klass) throws InitializationError {
super(klass);
}
@Override
protected Statement withBeforeClasses(Statement statement) {
// 추가 설정
System.out.println("Before All Tests");
return super.withBeforeClasses(statement);
}
}
테스트 클래스에서 커스텀 Runner 사용
@RunWith(CustomRunner.class) // Runner를 직접 지정
public class CustomRunnerTest {
@Test
public void testCustomLogic() {
assertEquals(2, 1 + 1);
}
}
JUnit5는 왜 더 편리할까⁉️
JUnit5에서는 확장을 적용하려면 @ExtendWith를 사용합니다. Runner를 직접 작성하지 않고, 확장 클래스만 작성하면 됩니다.
예를 들어, JUnit5에서는 아래와 같이 확장 클래스만 정의하여 테스트 클래스에서 필요한 기능을 쉽게 추가할 수 있습니다.
public class MyCustomExtension implements BeforeTestExecutionCallback {
@Override
public void beforeTestExecution(ExtensionContext context) {
System.out.println("Before Test Execution: " + context.getDisplayName());
}
}
테스트 클래스에서 확장 적용
@ExtendWith(MyCustomExtension.class) // 확장 클래스 적용
class CustomExtensionTest {
@Test
void testCustomLogic() {
assertEquals(2, 1 + 1);
}
}
JUnit5는 한 테스트 클래스에 여러 확장(@ExtendWith)을 적용할 수 있습니다.
반면, JUnit4는 한 번에 하나의 Runner만 사용할 수 있으므로 확장성이 제한됩니다.
가독성 부족
JUnit4에서는 테스트 메서드의 의미를 명확히 드러내기 어렵다는 점이 있었습니다. 예를 들어, 테스트 메서드 이름만으로 테스트의 목적을 이해하기 어려울 때가 있었습니다.
JUnit4의 테스트 코드
@Test
public void additionTest() {
assertEquals(2, 1 + 1);
}
JUnit5에서 @DisplayName을 활용한 테스트 코드
@DisplayName("간단한 덧셈 테스트")
@Test
void testAddition() {
assertEquals(2, 1 + 1);
}
JUnit5의 @DisplayName은 테스트 이름을 명시적으로 작성할 수 있어,
테스트 의도를 더 명확히 표현하고, 결과를 한눈에 파악하기 쉽게 만들어줍니다.
Mock 객체 활용의 복잡성
JUnit4에서 반복된다는 의미
JUnit4에서는 Mock 객체를 활용하려면 항상 @RunWith(MockitoJUnitRunner.class)를 사용해야 했습니다.
이것은 테스트 클래스마다 지정해주어야 하며, 커스텀 Runner를 사용할 경우 중복 설정이 발생합니다.
예를 들어, 아래 테스트 클래스는 Mock 객체를 사용하려고 MockitoJUnitRunner를 반드시 지정해야 합니다.
@RunWith(MockitoJUnitRunner.class) // 이 부분은 모든 Mock 객체 사용 테스트에서 필요
public class MockExampleTest {
@Mock
private SomeService someService;
@Test
public void testService() {
when(someService.getData()).thenReturn("Mock Data"); // Mock 데이터 설정
assertEquals("Mock Data", someService.getData());
}
}
만약 다른 Runner(예: SpringJUnit4 ClassRunner)를 사용해야 하는 테스트 환경에서는 Mock 객체 사용을 위한 추가 설정이 필요합니다.
결과적으로 Mock 객체 설정이 반복되거나 추가 코드로 인해 코드가 복잡해질 수 있습니다.
JUnit5는 어떻게 해결했을까⁉️
JUnit5에서는 @ExtendWith(MockitoExtension.class)를 사용합니다.
Runner 대신 확장 기능(Extension)으로 Mocking을 지원하므로, Mock 객체 설정과 테스트 환경을 더 유연하게 결합할 수 있습니다.
예를 들어, 아래처럼 Mock 객체 설정과 다른 확장 기능(예: Spring의 확장)도 함께 적용할 수 있습니다.
@ExtendWith(MockitoExtension.class) // Mock 객체 설정
@ExtendWith(SpringExtension.class) // Spring 확장
class MockExampleTest {
@Mock
private SomeService someService;
@Test
void testService() {
when(someService.getData()).thenReturn("Mock Data");
assertEquals("Mock Data", someService.getData());
}
}
💡 JUnit4는 간단한 테스트 환경에서는 충분히 효과적이었지만, 확장성과 가독성 측면에서 JUnit5가 더 나은 경험을 제공한다고 느꼈습니다. 특히, JUnit5의 간단한 설정과 명확한 테스트 표현 방식은 테스트 작성 및 유지보수의 효율성을 크게 높여줄 수 있을 것이라 생각합니다.
'TestCode' 카테고리의 다른 글
인수 테스트 격리하는 방법 (0) | 2024.07.06 |
---|---|
FixtureMonkey 알아보기 (0) | 2024.03.26 |
단위 테스트 vs 통합 테스트 vs 인수테스트 (1) | 2024.02.10 |