본문 바로가기

Spring

[Spring] 통합테스트와 Junit과 Mockito 프레임워크를 이용한 단위테스트 작성

테스트 코드를 작성하는 것은 개발단계에서 서비스 코드를 작성하는 것 만큼이나 매우 중요하다. 그 이유는 다음과 같다.

1. 작성한 코드가 의도한대로 동작하는지 알 수 있다.

작성한 코드를 테스트하는 방법은 포스트맨을 사용하거나 QA 과정에서 직접 서비스를 사용해 보면서 기능을 테스트하는 방법이 있는데 테스트 해야할 기능이 많다면 전부 테스트하는데 많은 시간이 걸린다. 하지만 테스트 코드를 미리 작성해 놓으면 애플리케이션을 실행하지 않고도 클릭 한번으로 테스트할 수 있으며 프로젝트 빌드 툴인 메이븐이나 그레이들 같은 툴을 사용하여 작성된 테스트 케이스들을 한 번에 모두 실행할 수도 있다.

2. 리팩토링 하기 쉬워진다.

리팩토링이란 결과값에 영향을 주지 않으면서 코드의 가독성과 유지보수성을 높이기 위해 내부 구조를 변경하는 작업이다. 그렇기 때문에 리팩토링을 하고나면 전과 동일한 결과를 출력하는지 테스트를 수행해야 하는데 테스트 코드를 통해 쉽게 테스트 할 수 있다.

 

그러므로 신뢰성 있는 애플리케이션을 개발하기 위해서는 애플리케이션을 자동으로 테스트하는 테스트 케이스를 작성하고 유지 보수하는 것이 매우 중요하다.

테스트 종류

테스트 방식으로는 크게 단위 테스트와, 통합 테스트로 구분할 수 있는데 메서드 단위로 테스트 하는 것을 단위 테스트(unit test), 애플리케이션의 기능이나 API 단위로 테스트하는 것을 통합 테스트(integration test)라고 한다.

좋은 단위 테스트를 작성하기 위한 5가지 원칙

좋은 단위 테스트를 작성하기 위해 지켜야할 5가지 원칙이 존재하는데 이를 F(Fast), I(Isolated), R(Repeatable), S(Self-validating), T(Timely)라고 하고 각각의 의미는 다음과 같다.

First

테스트 케이스는 빠르게(fast) 동작해야 한다. 실행 시간이 오래 걸리는 테스트 케이스는 성공 여부를 빠르게 확인할 수 없어 개발 시간에 영향을 미친다.

Isolated

테스트 케이스는 다른 외부 요인에 영향을 받지 않도록 격리(isolated)해야 한다. 즉, 테스트 케이스 사이에 서로 영향을 주는 테이트 케이스를 작성하면 안된다. 만약 다른 테스트 코드에 의존하거나 상호 동작한다면 신뢰할 만한 테스트 결과를 얻을 수 없다.

Repeatable

테스트 케이스는 반복(repeat)해서 실행하고, 실행할 때마다 같은 테스트 결과를 보장해야 한다. 만약 테스트 케이스를 실행할 때마다 다른 결과가 나온다면 테스트 과정 자체를 신뢰할 수 없다.

Self-validating

테스트 케이스 내부에는 결과 값을 자체 검증(self-validating)할 수 있는 코드가 필요하다. 즉, 테스트 결과 값을 개발자가 직접 기대하는 값과 예상 결과 값을 비교해야 한다면 테스트 과정을 자동화할 수 없다.

Timely

실제 코드를 개발하기 전 테스트 케이스를 먼저 작성하는 것을 의미한다. 이는 개발 단계부터 계속해서 테스트를 하면서 요구 사항에 적합한 코드를 만들 수 있는 장점이 있고 테스트 주도 개발 방법론(Test Driven Development: TDD)에 적합하다.

테스트 도구

Junit

자바 언어 환경에서 제공하는 테스트 라이브러리이다. 테스트 케이스를 작성하고 실행할 수 있는 기능들을 제공한다. 테스트 케이스를 정의할 수 있는 애너테이션과 실행한 테스트 결과 값을 예상 값과 비교 및 검증할 수 있는 클래스들을 제공한다.

 

Junit 사용 예제

public class MiscTest {

    @BeforeAll
    public static void setup() {
        System.out.println("before all tests in the current test class");
    }

    @BeforeEach
    public void inti() {
        System.out.println("before each @Test");
    }


    @Test
    public void testHashSetContainsNonDuplicatedValue() {

        // Given
        Integer value = 1;
        Set<Integer> set = new HashSet<>();

        // When
        set.add(value);
        set.add(value);
        set.add(value);

        // Then
        Assertions.assertEquals(1, set.size());
        Assertions.assertTrue(set.contains(value));
    }

    @Test
    public void testDummy() {
        Assertions.assertTrue(Boolean.TRUE);
    }

    @AfterEach
    public void cleanup() {
        System.out.println("after each @Test");
    }

    @AfterAll
    public static void destroy() {
        System.out.println("after all tests in the current test class");
    }

}

 

@BeforeAll

테스트 클래스 인스턴스를 초기화할 때 가장 먼저 실행된다. 테스트 클래스에 포함된 테스트 메서드가 여러 개 있어도 한번만 실행되며 객체를 생성하기 전에 미리 실행해야 하므로 static 키워드를 사용해서 정의해야 한다.

 

@BeforeEach

모든 테스트 메서드가 실행되기 전 각각 한 번씩 실행된다. 즉, testHashSetContainsNonDuplicatedValue() 메서드와 testDummy() 메서드에서 각각2번 실행된다.

 

 

@AfterEach

모든 테스트 메서드가 실행된 후 각각 한 번씩 실행된다.

 

@AfterAll

테스트 클래스의 모든 테스트 메서드가 실행을 마치면 마지막에 한 번만 실행된다. @AfterAll도 @BeforeAll과 마찬가지로 static 키워드를 사용해서 정의해야 한다.

 

@Test

@Test 어노테이션이 부여된 메서드는 테스트 대상으로 지정된다.

 

테스트 결과 검증 메서드

Juit은 테스트 결과를 예상한 값과 자동으로 비교하여 검증할 수 있게 Assertions라는 스테틱 검증 메서드를 제공한다.

  • assertNull(Object actual) : 실제 값이 Null인지 검증한다.
  • assertNotNull(Object actual) : 실제 값이 Not null인지 검증한다.
  • assertTrue(boolean condition) : 조건이 참인지 검증한다.
  • assertFalse(boolean condition) : 조건이 거짓인지 검증한다.
  • assertEquals(Object expect, Object actual) : 예상 값과 실제 값이 같은지 비교한다. (동등성 비교)
  • assertNotEquals(Object expect, Object actual) : 예상 값과 실제 값이 다른지 비교한다. (동등성 비교)
  • assertSame(Object expect, Object actual) : 예상 값과 실제 값이 같은지 비교한다. (동일성 비교)
  • assertNotSame(Object expect, Object actual) : 예상 값과 실제 값이 다른지 비교한다. (동일성 비교)

Mockito

테스트에서 사용할 수 있는 목(mock) 프레임워크이다. 목 객체는 개발자가 입력 값에 따라 출력 값을 프로그래밍한 가짜 객체이다. Mockito 프레임워크는 테스트 대상 클래스가 의존하는 객체를 목 객체로 바꿀 수 있는 기능과 목 객체를 만들 수 있는 기능을 제공한다. 그래서 Junit과 함께 많이 사용되는 라이브러리이며 테스트 환경 설정부터 테스트 검증까지 테스트 전체 과정 모두를 처리할 수 있는 기능을 제공한다.

 

Mockito 사용 예제

@ExtendWith(MockitoExtension.class)
class ReviewServiceImplUnitTest {

    @Mock
    private ReviewRepository reviewRepository;

    @InjectMocks
    private ReviewServiceImpl reviewService;

    @Test
    @DisplayName("리뷰 상세 조회 성공")
    public void getDetailReview() {

        // Given
        Long reviewId = 1L;
        ReviewRequestDto requestDto = new ReviewRequestDto(
                "title",
                "content",
                "question",
                "answer"
        );

        Review review = Review.of(getMember(), getCompany(), requestDto);

        given(this.reviewRepository.findById(any()))
                .willReturn(Optional.ofNullable(review));

        // When
        ReviewResponseDto reviewResponseDto = reviewService.getDetailReview(reviewId);

        // Then
        Assertions.assertEquals("title", reviewResponseDto.getTitle());
        Assertions.assertEquals("content", reviewResponseDto.getContent());
        Assertions.assertEquals("question", reviewResponseDto.getQuestion());
        Assertions.assertEquals("answer", reviewResponseDto.getAnswer());
    }

    private Member getMember() {
        return Member.builder()
                .email("test@test.com")
                .password("1111")
                .name("kiyoom")
                .build();
    }

    private Company getCompany() {
        return Company.builder()
                .companyName("삼성")
                .companyUrl("www.samsung.com")
                .companyAddress("삼성시")
                .employeeCnt(500)
                .companyDesc("반도체")
                .build();
    }
}

 

테스트 성공

 

단위테스트는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트로 스프링을 실행하지 않습니다. 

 

@ExtendWith(MockitoExtension.class)

이 테스트 클레스가 Mockito 프레임워크를 사용하겠다는 것을 의미합니다.

 

@Mock

가짜 객체를 선언하는 것을 의미합니다. 테스트 시 실제 객체 대신 @Mock 어노테이션이 선언된 가짜 객체가 주입되어 단위테스트가 실행됩니다.

 

@InjectMock

@Mock 어노테이션이 선언된 가짜 객체가 주입될 클래스를 의미합니다.

 

@MockBean

mock객체를 ApplicationContext에 등록해준다. 그리고 ApplicationContext는 목 객체를 주입받기 원하는 스프링 빈이 있다면 해당 목 객체를 주입해준다. 만약 ApplicationContext에 목 객체와 같은 클래스 타입과 이름이 같은 스프링 빈이 있다면 해당 객체는 목 객체로 바뀐다. 

 

스텁(stub)

프로그래밍할 수 있으며 개발자가 원하는 결과를 응답하는 메서드를 의미한다.

 

BDDMockito.given()

스텁을 만드는 메서드로 given()의 인자에 스텁으로 만들 대상 메서드를 입력한다. 테스트하고자 하는 클래스의 메서드에서 스텁 메서드를 호출하면 프로그래밍된 결과가 응답한다.

 

ArgumentMatchers.any()

any()를 사용하면 어떤 인자 값을 사용하더라도 given()으로 만들어진 스텁이 동작한다.  

 

BDDMockito.willReturn()

given()으로 선언된 스텁이 호출되면 willReturn() 메서드의 인자로 입력된 값을 응답한다.

spring-boot-test

스프링 부트 프레임워크의 기능을 통합 테스트할 수 있는 기능을 제공한다.

@SpringBootTest
@Transactional
class ReviewServiceImplTest {

    @Autowired
    private ReviewService reviewService;

    @Test
    @DisplayName("라뷰 작성 성공")
    public void writeReviewSuccessTest() {

        // Given
        String email = "test@test.com";
        Long companyId = 1L;
        ReviewRequestDto requestDto = new ReviewRequestDto(
                "title",
                "content",
                "question",
                "answer"
        );

        // When
        ReviewResponseDto reviewResponseDto = reviewService.writeReview(requestDto, email, companyId);

        // Then
        Assertions.assertEquals("title", reviewResponseDto.getTitle());
        Assertions.assertEquals("content", reviewResponseDto.getContent());
        Assertions.assertEquals("question", reviewResponseDto.getQuestion());
        Assertions.assertEquals("answer", reviewResponseDto.getAnswer());
    }

}

 

테스트 성공

 

@Transactional

테스트 클래스에서 @Transactional을 선언하면 테스트가 종료될 때 데이터베이스를 초기화해준다. 테스트 클래스는 반복사용할 수 있어야 하기 때문에 데이터베이스를 초기화하여 다음 테스트에 영향을 주지 않도록 해야한다.

 

@SpringBootTest

SpringBootTest를 수행하기 위해서는 테스트 대상 클래스에 @SpringBootTest 어노테이션을 부여해주어야 한다. @SpringBootTest 어노테이션을 테스트 클래스에 선언하면 @SpringBootApplication 어노테이션이 적용된 클래스를 찾아 @SpringBootTest에 설정된 속성과 함꼐 애플리케이션에 선언된 스프링 빈들도 스캔하고 생성한다. 그렇기 때문에 SpringBootTest를 실행하면 아래와 같이 Spring이 실행된다.

 

 

Junit4를 사용한다면 아래와 같이 테스트 클래스에 @RunWith(SpringRunner.class) 설정을 같이 사용해야하고 Junit5를 사용한다면 @RunWith 설정을 생략해도 된다.

@SpringBootTest
@Transactional
@RunWith(SpringRunner.class)
class ReviewServiceImplTest {
    ....
}

 

@Autowired

SpringBootTest는 테스트하고자 하는 클래스가 다른 스프링 빈에 의존하고 있기 때문에 ApplicationContext가 테스트 대상 클래스가 의존하는 적절한 스프링 빈들을 생성하고 스프링 빈을 주입해야 테스트할 수 있다. 그렇기 때문에 반드시 생성자에 @Autowired 어노테이션을 정의해서 스프링 빈을 주입받아야 한다.