본문 바로가기

내배캠/TIL

[스프링] 테스트

테스트 코드는 코드 품질을 위해 선택이 아닌 필수로 진행해야 하는 과정 중 하나이다.

개발자는 다양한 시나리오에서 코드가 예상대로 작동하는지 확인하여 소프트웨어 품질을 유지하고 향상시킬 수 있다. 

또한 새로운 요구사항으로 기존 코드가 수정되더라도 테스트 코드를 이용해 빠른 시간 안에 버그를 발견할 수 있고 기존 기능이 그대로 잘 동작하는지 확인이 가능하다. 

 

 

Given - When - Then 패턴

 

 

Junit5

  • 자바(Java) 언어에서 사용되는 단위 테스트 프레임워크
  • Junit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
    • @BeforeAll, @BeforeEach, @Test, @AfterEach, @AfterAll 제공

 

 

 

Mock

Mockito

  • Mock = 가짜
  • Mockito는 Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크
  • Mock을 기반으로 작성된 테스트 코드는 인터넷이 안되는 비행기 안에서도 동작 및 검증 가능
  • 통합테스트인 @SpringBootTest 와 다르게 별도 환경, 구성에 대한 제약 사항이 없기 때문에 매우 빠르게 동작

 

 

Unit Test

  • 단위(Unit) 테스트에서 말하는 단위(Unit)는 애플리케이션에서 가장 작은 테스트 가능 요소를 뜻함
    → 함수(Method)
  • 실제 객체를 사용하지 않고 Mocking을 함으로써 의존성이 적고 빠르게 테스트를 할 수 있음
  • FIRST 원칙
      • Fast: 유닛 테스트는 빨라야 함
      • Isolated: 테스트는 각 테스트간에 독립적으로 실행해야함
      • Repeatable: 테스트는 환경에 상관없이 실행할 때마다 같은 결과를 만들어야 함.
      • Self-validating: 테스트는 명확히 성공/실패로 구분하여 테스트 자체가 결과를 검증할 수 있어야함.
      • Timely: 테스트는 개발간에 즉시 작성해야 함. 대표적으로 TDD 방법론이 있음. (쉽지 않음)

 

Mock Unit Test

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.BDDMockito.given;

@ExtendWith(MockitoExtension.class)
public class MovieServiceTest {
    @Mock
    MovieRepository movieRepository;

    @InjectMocks
    MovieService movieService;

    @Test
    @DisplayName("영화 단건조회 테스트")
    public void getMovieTest() {
        // given
        int movieId = 1;
        Movie movie = new Movie(
            new Director("ahn"),
            List.of(new Actor("park"), new Actor("kim"))
        );

        given(movieRepository.findById(anyLong())).willReturn(Optional.of(movie));

        // when
        MovieResponse result = movieService.getMovie(movieId);

        // then
        assertNotNull(result);
    }
}

 

Mocking

@ExtendWith(MockitoExtension.class)
public class MovieServiceTest {
    @Mock
    MovieRepository movieRepository;

    @InjectMocks
    MovieService movieService;

    @Test
    @DisplayName("영화 단건조회 테스트")
    public void getMovieTest() {
        ...
    }
}

 

  • @Mock: 테스트에서 사용될 가짜 객체. 해당 객체의 메서드를 모방 하기 위한 용도
  • @InjectMocks: Mock 객체를 주입하여 실제 객체의 의존성을 자동으로 설정함. Mock이 아닌 실제 객체.
  • Mocking은 실제 객체 대신 가짜 객체를 만들어 테스트 중에 해당 객체의 동작을 시뮬레이션하는 과정

 

Class/Method Naming

@Test
@DisplayName("displayName 영화 단건 조회")
public void getMovieTest() {
    ...
}

@Test
public void 함수명_영화_단건조회() {
    ...
}
  • 클래스명은 {TargetClass}Test 형태로 작성하는 편
  • 함수명
    • @DisplayName을 사용하고 함수명을 영문 테스트명을 작성
    • @DisplayName을 사용하지 않고 한글로 테스트명을 작성(언더바 처리가 필요)
    • 둘다 자주 사용하는 방식이고, 정답은 없음. 개발간에는 취향따라, 실무에서는 회사 또는 팀 규정따라.

     

given

@Test
public void 영화_단건조회() {
    // given
    long movieId = 1L;
    given(movieRepository.findById(anyLong())).willReturn(Optional.of(movie));
    
    ...
}
  • given에서 자원을 주어줄 때, mock에 어떤 값이든(any) 할당하는 값 메소드
  • willRetrun으로 결과 값을 매핑하기 때문에 자원에는 어떠한 값이 매핑되어도 상관 없기 때문에 사용함
  • any(), anyLong(), anyString(), anyList(), anySet(), anyMap() 등이 있음

 

when

@Test
@DisplayName("영화 단건조회 테스트")
public void getMovieTest() {
    ...

    // when
    MovieResponse result = movieService.getMovie(movieId);

    ...
}
  • @InjectMocks 대상 객체는 mock 객체가 아님
  • @InjectMocks 대상은 테스트하려는 실제 객체를 생성하고, 그 객체의 의존성들만 mocking된 객체로 대체하는 것
  • 따라서 @InjectMocks 대상 객체에는 mock에 넣어주는 any() 가 아닌 실제 자원을 넣어줘야함

 

then

@Test
@DisplayName("영화 삭제 실패")
public void getMovieTest() {
    ...

    // then
    assertEquals("entity null error", exception.getMessage());
    verify(movieRepository, times(0)).delete(any(Movie.class));
}
  • assert: 테스트의 결과를 검증하는 데 사용하며, 테스트 예상한 결과와 실제 결과와 비교하여 테스트가 성공/실패를 결정
  • verify: 특정 메소드가 호출되었는지, 호출 횟수는 몇 번인지, 호출 순서는 어떤지 등을 검증하는 데 사용

 

throws

@Test
public void 영화단건_조회() {
    // given
    long movieId = 1L;
    given(movieRepository.findById(anyLong())).willReturn(Optional.empty());

    // when
    NullPointerException exception = assertThrows(NullPointerException.class, () -> movieService.getMovie(movieId));

    // then
    assertEquals("null error", exception.getMessage());
}
  • assertThrows 로 처리하며, 각 상황에 정의한 에러 메세지가 맞는지 확인함

 

void method

@Test
public void 영화_삭제() {
    // given
    long movieId = 1L;
    Movie movie = new Movie("재밌는영화", 2002);

    given(movieRepository.findById(anyLong())).willReturn(Optional.of(movie));
    doNothing().when(movieRepository).delete(any(Movie.class));

    // when
    movieService.removeMovie(movieId);

    // then
    verify(movieRepository, times(1)).delete(any(Movie.class));
}
  • Mocktito.doNothing 메소드로 아무것도 처리 하지 않겠다는 의미
  • verify와 함께 핵심 자원만 호출 되었는지 정도 확인 해주면 좋음

 

spy

import org.mockito.Spy;

@ExtendWith(MockitoExtension.class)
public class MovieServiceTest {
    @Mock
    MovieRepository movieRepository;

    @Spy
    private LogService logService;

    @InjectMocks
    MovieService movieService;

    @Test
    @DisplayName("영화 단건조회 테스트")
    public void getMovieTest() {
        ...
    }
}
  • @Spy: 마치 첩보 요원 처럼, 실제 객체의 행동을 하면서 우리가 조작한 행동(stubbing)은 우리가 설정한대로 동작
  • 실제 객체를 사용하면서 일부 기능에 대한 mock 처리가 필요할 때 사용

 

Nested

import org.junit.jupiter.api.Nested;

@ExtendWith(MockitoExtension.class)
public class MovieServiceTest {
    ...

    @Nested
    class GetMovieTest {
        @Test
        public void 영화단건조회_조회결과없음() {
            ...
        }

        @Test
        public void 영화단건조회_정상조회() {
            ...
        }
    }


    @Nested
    class DeleteMovieTest {
        @Test
        public void 영화삭제_조회결과없음() {
            ...
        }

        @Test
        public void 영화삭제_정상동작() {
            ...
        }
    }
}
  • @Nested 중첩 클래스를 통해 테스트 묶음 단위 설정 가능
  • 서로 관련 있는 Unit 테스트간 묶음을 통해 테스트 코드 가독성 증가
  • 묶음 단위 내에서의 순서를 통해 테스트 시나리오 작성 가능

 

WebMvc Test

WebMvc

@WebMvcTest bean 목록(공식)

i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans.

  • @WebMvcTest는 전체 테스트 하는 것이 아니라 Web layer만을 테스트할 때 사용

 

Controller Test

@WebMvcTest(MovieController.class)
public class MovieControllerTest {
    @MockBean
    private MovieService movieService;

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void 영화_단건_조회() throws Exception {
        // given
        long movieId = 1L;
        given(movieService.getMovie(anyLong())).willReturn(new MovieResponse(1L, "재밌는영화", 1992));

        // when
        ResultActions result = mockMvc.perform(get("/api/v1/movies/{movieId}", movieId));

        // then
        result.andExpect(status().isOk());
    }
}
  • @MockBean: Spring 컨텍스트에서 관리되는 Bean을 mock 객체로 교체함
  • 특정 컨트롤러나 웹 계층에 대한 단위 테스트 진행
  • 컨트롤러가 예상대로 작동하는지, 웹 요청과 응답이 제대로 이루어지는지 검증

 

SpringBoot Test

@SpringBootTest
public class MovieServiceTest {
    @Autowired
    MovieService movieService;

    @Test
    @DisplayName("영화 단건조회 테스트")
    public void getMovieTest() {
        // given
        int movieId = 1;

        // when
        MovieResponse movie = movieService.getMovie(movieId);

        // then
        assertNotNull(movie);
    }
}
  • SpringBoot의 Integration Test (통합 테스트)
  • 애플리케이션이 실제로 실행될 때처럼 빈(Bean)들이 초기화되고 설정된 환경에서 테스트를 수행
  • 실제 설정한 환경 구성에 따른 실제 데이터를 사용하기 때문에 실제로 등록된 데이터에 대한 실제 로직이 수행됨

 

Test Coverage

  • 애플리케이션의 테스트 케이스가 얼마나 충족되었는지를 나타내는 지표
  • 커버리지가 항상 100%가 될 필요는 없음. 사실상 불가함.
  • 커버리지 자체가 목적이 되기보다는, 중요한 로직과 엣지 케이스에 대한 충분한 테스트가 필요
  • 커버리지가 높다고 해서 항상 코드가 완벽하다는 보장은 없으며, 테스트의 품질이 더 중요

 

'내배캠 > TIL' 카테고리의 다른 글

[SQL] 오프라인/온라인 판매 데이터 통합하기  (1) 2024.09.14
트랜잭션 격리 수준  (0) 2024.09.12
[스프링] AOP  (0) 2024.09.10
카카오 로그인 개발하기  (5) 2024.09.09
JPA의 N+1 문제 해결하기  (0) 2024.09.06