JUnit

서버란·2024년 9월 18일

자바 궁금증

목록 보기
20/35

JUnit은 자바에서 널리 사용되는 단위 테스트 프레임워크로, 테스트 코드를 작성하고 실행하는 데 매우 유용합니다.
JUnit을 사용하면 개별 메서드나 클래스가 의도한 대로 동작하는지 검증할 수 있습니다.
JUnit 4와 JUnit 5가 많이 사용되며, JUnit 5는 JUnit 4에 비해 더 많은 기능과 유연성을 제공합니다.

JUnit에서 자주 사용되는 주요 어노테이션 및 메서드

  1. JUnit 4와 JUnit 5의 차이점
  • JUnit 4는 @Test, @Before, @After 등의 어노테이션을 사용하고, JUnit 5는 이와 유사하게 @Test, @BeforeEach, @AfterEach 등을 제공합니다.
  • JUnit 5에서는 더 세분화된 라이프사이클 어노테이션과 확장 가능성이 강화되었습니다.
  1. JUnit 5의 주요 어노테이션
  • @Test: 테스트 메서드를 정의합니다. 이 어노테이션이 붙은 메서드는 테스트 메서드로 간주됩니다.
@Test
void testMethod() {
    // 테스트 코드
}
  • @BeforeEach: 각 테스트 메서드가 실행되기 전에 실행될 코드를 정의합니다. 테스트 환경을 초기화하는 데 주로 사용됩니다.
@BeforeEach
void setUp() {
    // 초기화 코드
}
  • @AfterEach: 각 테스트 메서드가 실행된 후에 실행될 코드를 정의합니다. 자원을 정리하거나 초기 상태로 되돌리는 데 사용됩니다.
@AfterEach
void tearDown() {
    // 정리 코드
}
  • @BeforeAll: 모든 테스트가 실행되기 전에 한 번만 실행되는 메서드를 정의합니다. 정적 메서드여야 합니다.
@BeforeAll
static void init() {
    // 전체 테스트에 앞서 한 번 실행될 코드
}
  • @AfterAll: 모든 테스트가 끝난 후에 한 번만 실행되는 메서드를 정의합니다. 이 역시 정적 메서드로 선언해야 합니다.
@AfterAll
static void cleanUp() {
    // 전체 테스트가 끝난 후 한 번 실행될 코드
}
  • @DisplayName: 테스트 메서드나 클래스에 커스텀 이름을 지정할 수 있습니다. 테스트 실행 시 테스트의 목적을 명확하게 표현할 수 있습니다.
@Test
@DisplayName("1 + 1은 2가 되어야 한다")
void testAddition() {
    assertEquals(2, 1 + 1);
}

@Disabled: 특정 테스트를 비활성화할 때 사용됩니다. 테스트는 작성되어 있지만, 실행하고 싶지 않을 때 이 어노테이션을 사용합니다.

@Test
@Disabled("아직 구현되지 않음")
void testDisabled() {
    // 이 테스트는 실행되지 않음
}
  1. JUnit의 주요 메서드
  • Assertions (단정문): JUnit에서 가장 중요한 것은 테스트 대상 코드가 예상한 대로 동작하는지 검증하는 것입니다. 이를 위해 JUnit은 여러 가지 단정문을 제공합니다.
  • 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이 아닌지 확인
}
  1. 테스트 클래스의 구성 테스트 클래스는 여러 테스트 메서드를 포함할 수 있습니다. 일반적으로 각 테스트 메서드는 독립적이어야 하며, 특정 순서에 의존하지 않도록 설계하는 것이 좋습니다. 예를 들어, 데이터베이스와 연동되는 테스트라면 테스트 환경을 초기화하고 종료할 때 데이터베이스를 리셋하는 작업이 필요할 수 있습니다.

  2. Mocking과 Stubbing 테스트할 때 실제 의존성을 사용하는 대신, 가짜 객체(mock)를 사용할 수 있습니다. 이를 통해 의존성을 격리하고 특정 조건에서 코드를 테스트할 수 있습니다. 이를 위해 Mockito와 같은 라이브러리가 많이 사용됩니다.

@Test
void testWithMock() {
    List<String> mockList = mock(List.class);
    when(mockList.size()).thenReturn(5);
    
    assertEquals(5, mockList.size());
}

JUnit을 사용하여 테스트 코드를 작성할 때의 좋은 습관

  • 테스트는 독립적이어야 한다: 각 테스트는 서로 의존하지 않고 독립적으로 실행될 수 있어야 합니다. 특정 테스트가 실패하더라도 다른 테스트에 영향을 주지 않아야 합니다.
  • 테스트 환경을 적절하게 설정 및 정리하기: @BeforeEach와 @AfterEach 어노테이션을 사용하여 테스트 전후에 필요한 환경 설정 및 정리를 수행합니다.
  • 단위 테스트는 가능한 작게: 단위 테스트는 작은 범위의 기능을 테스트해야 합니다. 이를 통해 문제 발생 시 쉽게 원인을 파악할 수 있습니다.
  • 테스트 네이밍: 테스트 메서드의 이름은 해당 메서드가 무엇을 테스트하는지 명확하게 표현해야 합니다. 이를 통해 테스트 실패 시 문제가 되는 부분을 빠르게 파악할 수 있습니다.

이와 같은 원칙을 지키면 JUnit을 통해 효율적으로 테스트 코드를 작성할 수 있습니다.


Q1. JUnit 테스트에서 assertThrows 메서드를 사용할 때 주의해야 할 점은 무엇인가요?

assertThrows는 특정 메서드가 예외를 발생시키는지 확인할 때 사용합니다. 이 메서드를 사용할 때 주의할 점은 아래와 같습니다:

  1. 예외 타입 일치: 발생하는 예외가 기대한 예외 타입과 일치해야 합니다. assertThrows(IllegalArgumentException.class, ...)와 같이 예외 타입을 명시합니다. 만약 다른 예외가 발생하거나 예외가 발생하지 않으면 테스트는 실패합니다.
@Test
void testException() {
    assertThrows(IllegalArgumentException.class, () -> {
        throw new IllegalArgumentException("예외 발생");
    });
}
  1. 실행 코드 위치: assertThrows의 두 번째 인자는 예외를 발생시킬 실행 코드입니다. 이 코드는 람다 표현식으로 작성되며, 예외를 유발하는 코드만 그 안에 포함되어야 합니다.
assertThrows(IllegalArgumentException.class, () -> someMethod("invalid input"));
  1. 예외 메시지 검증: 예외 발생 여부뿐 아니라, 예외 메시지나 상태도 검증하고 싶다면 assertThrows로 반환된 예외 객체를 이용해 추가 검증이 가능합니다.
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
    throw new IllegalArgumentException("Invalid argument");
});
assertEquals("Invalid argument", thrown.getMessage());

Q2. Mockito를 이용해 의존성을 목(mock)으로 처리할 때 어떤 상황에서 유용하게 사용할 수 있나요?

Mockito는 주로 의존성이 있는 클래스나 객체를 테스트할 때 유용합니다. 다음과 같은 상황에서 사용할 수 있습니다:

  1. 외부 의존성의 분리: 데이터베이스, 외부 API, 파일 시스템 등 외부 의존성은 테스트에서 분리하는 것이 좋습니다. 실제 데이터베이스에 접근하거나 외부 API를 호출하는 것은 비용이 크거나 복잡할 수 있으므로, 목 객체를 사용하여 이와 같은 의존성을 모사할 수 있습니다.
@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());
}
  1. 복잡한 비즈니스 로직 테스트: 서비스 레이어나 비즈니스 로직을 테스트할 때 여러 객체 간의 상호작용을 목 객체로 처리하여 해당 로직만 집중적으로 테스트할 수 있습니다. 예를 들어, 서비스에서 여러 리포지토리나 API를 호출하는 경우, 각 의존성을 Mockito로 모킹하여 독립적인 테스트를 작성할 수 있습니다.

  2. 성능 및 상태 제어: 목 객체를 이용하면 테스트를 빠르고 효율적으로 수행할 수 있습니다. 예를 들어, 실제 파일을 읽고 쓰는 동작을 모킹하여 테스트 시간과 성능을 향상시킬 수 있습니다.

  3. 예외 상황 테스트: 목 객체는 특정 조건에서 예외를 던지도록 설정할 수 있기 때문에, 예외 상황도 쉽게 테스트할 수 있습니다. 이를 통해 에러 처리 로직을 검증할 수 있습니다.

when(mockRepo.findById(anyInt())).thenThrow(new IllegalArgumentException("Invalid ID"));

Q3. JUnit에서 테스트 메서드들이 독립적이어야 하는 이유는 무엇인가요?

테스트 메서드들이 독립적이어야 하는 이유는 아래와 같습니다:

  1. 테스트의 신뢰성: 테스트가 독립적이지 않으면, 한 테스트의 결과가 다른 테스트에 영향을 미칠 수 있습니다. 예를 들어, 어떤 테스트가 데이터를 수정한 후 그 상태가 다음 테스트에 영향을 준다면, 테스트 실패 원인을 파악하기 어렵습니다. 각 테스트는 독립적으로 실행될 수 있어야 신뢰성 높은 테스트가 가능합니다.

  2. 테스트 유지 보수: 독립적인 테스트는 유지 보수가 쉽습니다. 테스트 메서드들이 서로 의존하면, 한 메서드의 변경이 다른 테스트에도 영향을 미칠 수 있습니다. 이는 테스트 유지 보수를 어렵게 만들고, 작은 코드 변경이 여러 테스트 실패를 유발할 수 있습니다.

  3. 병렬 실행 가능성: 테스트가 독립적일 경우, 여러 테스트를 병렬로 실행할 수 있습니다. 이는 전체 테스트 시간을 단축시키고, CI(지속적 통합) 환경에서 성능 향상에 기여합니다.

  4. 재현 가능성: 독립적인 테스트는 어떤 순서로 실행되든 항상 동일한 결과를 보장합니다. 의존성이 있는 테스트는 실행 순서에 따라 결과가 달라질 수 있으며, 이는 재현하기 어려운 버그를 초래할 수 있습니다.

profile
백엔드에서 서버엔지니어가 된 사람

0개의 댓글