ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 테스트 코드
    Programming/SpringBoot 2024. 5. 14. 13:51
    728x90

    단위 테스트, 통합 테스트

    단위 테스트 Unit Test

    작은 단위의 코드에 대해 테스트를 수행하는 것

    개발자가 설계하고 만든 코드가 원래 의도대로 동작하는지를 개발자가 스스로 빠리 확인 받기 위해서 한다.

     

    스프링부트에서의 단위 테스트와 통합 테스트를 구분하는 것으로 "@SpringBootTest 어노테이션 사용 유무"를 들 수 있다.

    @SpringBootTest 어노테이션을 사용하지 않는 것을 단위 테스트라고 하자.

    이는 스프링을 전혀 실행하지 않는다는 것이다. 디팬던시가 전혀 없는 상황에서 실행할 수 있는 테스트이다.

     

     

    통합 테스트 Integration Test

    @SpringBootTest 어노테이션을 사용한다면 통합 테스트라고 하자.

    @SpringBootTest는 스프링을 실행시켜야 한다. 따라서 Spring에서 사용하는 의존성을 가져와 사용할 수 있다.

     

    @SpringBootTest어노테이션을 사용하면 간편하게 스프링 애플리케이션 테스트를 할 수 있다. 기존에 사용하던 @ContentConfiguration의 대안으로, @SpringBootTest를 선언하면 스프링이 실행되고 ApplicationContext를 생성하여 작동한다. 기본적으로 @ExtendWith(SpringExtension.class) 와 함께 사용해야 한다.

     

    @ExtendWith

    단위 테스트에 공통적으로 사용할 확장 기능을 선언하는 역할

    인자로 확장할 Extendsion을 명시한다.

    • SpringExtend.class       : Spring Context프레임워크 + Junit5를 통합해 사용
    • MockitoExtention.class : Junit5 + Mockito 를 연동해 테스트를 진행할 경우

     

    @SpringBootTest

    @SpringBootTest어노테이션을 사용하면 필요한 Bean을 모두 올려서 테스트할 수 있다. 참고로, 별도의 클래스를 지정하지 않으면 전체 애플리케이션을 로드하고 모든 Bean을 생성한다. 

     

    @SpringBootTest어노테이션은 기본적으로 서블릿 서버를 실행하지 않는다.

    WebEnvironment속성을 사용하면 컨테이너가 서블릿 환경에서 작동되도록 할 수 있다. 기본 설정은 NONE이다.

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    • NOME : 기본 설정, ApplicationContext가 실행되지만 서블릿 웹 환경은 제공하지 않는다.
    • RANDOM_PORT : ServletWebServerApplicationContext를 실행하고 임베디드 서버를 실행하는데 랜덤 포트로 서버를 띄운다. 테스트가 끝나면 포트는 바로 내려간다.
    • DEFINED_PORT : ServletWebServerApplicationContext를 실행하고 임베디드 서버를 실행하는데 프로퍼티 설정에 의한 포트로 서버를 띄운다. 기본 포트는 8080이다.
    • MOCK : 임베디드 서버를 실행하지는 않지만, 가짜의 서블릿 환경을 제공한다.

    MVC를 위한 웹 테스트는 @WebMvcTest어노테이션을 사용하는 것을 추천한다고 한다.

     

    테스트의 의존성 주입

    생성자로는 의존성을 주입할 수 없다.

    Test에서는 왜 @Autowired없이는 의존성 주입이 안될까?

    주입하는 주체가 다르다!

    SpringApplication의 경우 빈을 주입해주는 역할을 스프링이 담당한다.

    하지만 테스트의 경우 Junit(Jupiter엔진)에 의해 객체를 주입하게 된다. 이 때 JUnit은 ParameterResolver를 사용해서 주입한다.

     

    Parameter Resolver

    테스트에 동적으로 parameter를 주입할 수 있도록 Junit에서 제공하는 인터페이스

    xxxExtention를 구현체로 사용하며 supportsParameter(), resolveParamter()메서드를 구현해야 한다.

    supportsParameter() : 주입할 수 있는 타입 여부를 boolean으로 리턴한다.

    resolverParameter() : 주입할 타입의 객체를 반환한다.

    사용하려면 @ExtendWith(xxxExtension.class)를 테스트 위에 붙여야 한다.

     

    @SpringBootTest, @JdbcTest 내부에는 @ExtendWith(SpringExtension.class) 어노테이션이 등록되어 있으며, SpringExtension은 ParameterResolver를 구현한 구현체이다.

    SpringExtension클래스의 supportParameter메서드에서는 스프링 컨테이너에서 가져올 수 있는 빈 타입 여부를 @Autowired어노테이션이 있는지를 가지고 판단한다.

     

    Junit에서 @Autowired를 사용하지 않으면 SpringExtension은 해당 타입의 객체를 제공할 수 없다고 응답하게 되고, Junit은 해당 타입을 처리할 ParmeterReoslver가 없다고 에러를 발생시킨다.

    그래서 결과적으로 @Autowired 없이는 객체의 주입이 불가능하다.

     

    Mokito

    Java오픈소스 테스트 프레임워크

    Mockito를 사용하면 실제 객체를 모방한 가짜 객체, Mock객체 생성이 가능하다.

     

    Mock : 가짜 객체 ( 실제 객체를 대신해주는 테스트용 객체)

     

    Mock객체 생성하는 방법

    @Autowired 어노테이션의 용법과 동일하게, Mock객체를 만들어 사용하고 싶은 클래스를 @Mock어노테이션 혹은 @MockBean어노테이션을 사용하여 필드 주입해주면 된다.

     

    Mockito.mock()

    Mockito.mock()메서드를 사용하면 클래스 또는 인터페이스의 mock객체를 생성할 수 있다.

    또한 mock객체를 사용해, 해당 객체가 가진 메서드의 반환 값을 조작하거나 해당 메서드가 호출되었는지 확인할 수 있다.

        @Test
        @DisplayName("중복된 이메일이 존재하지 않는 경우 False를 반환")
        void isNotDuplicatedEmailExist(){
            MemberJdbcRepository repository = Mockito.mock(MemberJdbcRepository.class);
            //given
            when(repository.findByEmail(any())).thenReturn(null);
            //when-then
            assertFalse(memberService.isDuplicatedEmail(memberDto.getEmail()));
        }

    mock()메서드를 사용해서 mocking된 객체와, 해당 객체의 메서드를 조작해 원하는 값으로 리턴값을 조작할 수 있다.

     

    @Mock

    Mockito.mock()메서드의 축약어

    테스트 클래스에서만 사용해야 한다.

    Mockito annotation을 활성화해야만 @Mock어노테이션을 사용할 수 있다.

     

    @Mock어노테이션 활성화 방법

    @ExtendWith(MockitoExtension.class)메서드로 활성화 할 수 있다. (JUnit5)

     

    Mocking을 설정할 때 @InjectionMocks를 사용하게 되면, mocking된 객체들을 어노테이션이 설정된 객체에 주입해주기에, mock객체 주입과 관련된 설정 코드의 양을 줄일 수 있다.

    import org.mockito.Mock;
    
    @ExtendWith(MockitoExtension.class)
    public class MemberServiceTest {
    
        @InjectMocks
        private MemberService memberService;
    
        @Mock
        private MemberJdbcRepository memberJdbcRepository;
    	....
    
        @Test
        @DisplayName("중복된 이메일이 존재하지 않는 경우 False를 반환")
        void isNotDuplicatedEmailExist(){
            //given
            when(memberJdbcRepository.findByEmail(any())).thenReturn(null);
            //when-then
            assertFalse(memberService.isDuplicatedEmail(memberDto.getEmail()));
        }

     

     

    @MockBean

    스프링 애플리케이션 컨텍스트에 Mock객체를 추가하기 위해 @MockBean을 사용할 수 있다.

    (스프링 애플리케이션 컨텍스트를 사용한다? -> SpringBoot의 의존성이 필요하다! -> @SpringBootTest가 있어야 사용할 수 있다!)

    해당 어노테이션으로 mocking된 객체는 스프링 애플리케이션 컨텍스트에서 동일한 유형을 가진 기존 빈을 대체한다.

    만약 동일한 유형의 빈이 정의되어 있지 않으면 새로운 Bean이 추가된다.

    이 어노테이션은 외부 서비스와 같은 특정 빈을 모킹해야 하는 통합 테스트 작성시에 유용하다.

    @MockBean어노테이션을 사용하려면 @ExtendWith(SpringExtension.class)를 사용하면 된다. (JUnit5)

     

    @MockBean의 경로는 "org.springframework.boot.test.mock.mockito.MockBean" 보면 알 수 있듯이 @Mock과는 다르게 Spring영역에 있는 어노테이션이라는 것을 알 수 있다.

    @MockBean은 스프링 컨텍스트에 mock객체를 등록하게 되고 스프링 컨텍스트에 의해 @Autowired가 동작할 때 등록된 mock 객체를 사용할 수 있도록 동작한다.

     

    @SpringBootTest + @MockBean

    @SpringBootTest어노테이션을 사용하여, 통합 테스트를 진행하면서 Mock객체를 사용해야 하는 경우가 있다. 

    import org.springframework.boot.test.mock.mockito.MockBean;
    
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    public class MemberServiceTest {
        @Autowired
        private MemberService memberService;
    
        @MockBean
        private MemberJdbcRepository memberJdbcRepository;
       ...
       
        @Test
        @DisplayName("중복된 이메일이 존재하지 않는 경우 False를 반환")
        void isNotDuplicatedEmailExist(){
            //given
            when(memberJdbcRepository.findByEmail(any())).thenReturn(null);
            //when-then
            assertFalse(memberService.isDuplicatedEmail(memberDto.getEmail()));
        }

    MemberJdbcRepository는 @Repository어노테이션이 붙어 있어 Bean으로 등록된 객체이다.

    repository에서는 DB와 연동되어 sql문이 동작하는 곳이다. 실제 repository를 사용한다면 db에 반영이 될 것이다.

    @MockBean 어노테이션으로 기존 빈을 Mock객체로 대체한다. 

    기존 빈을 MockBean으로 대체하여 실제 DB에 반영되지 않고 실제 환경을 테스트할 수 있다!

     

    @Mock 과 @MockBean차이?

    Mock 종류 의존성 주입 Target
    @Mock @InjecMocks
    @MockBean @SpringBootTest

     

    @Mock은 @InjecMocks에 대해서만 해당 클래스안에서 정의된 객체를 찾아서 의존성을 해결한다.

    @MockBean은 Mock객체를 스프링 컨텍스트에 등록하는 것이기 때문에 @SpringBootTest를 통해서 Autowired에 의존성이 주입된다.

    MockMVC

    스프링 프레임워크에서 제공하는 웹 애플리케이션 테스트용 라이브러리이다.

    MockMVC를 사용하면 HTTP요청을 작성하고 컨트롤러의 응답을 검증할 수 있다.

    컨트롤러의 동작을 테스트하는데 사용된다.

     

    컨트롤러의 엔드포인트를 호출하여 HTTP클라이언트의 요청을 모방하고 적절한 응답을 확인하기 위해 테스트를 수행한다. 

    애플리케이션의 서비스 로직이나 API엔드포인트가 의도한 대로 동작하는지 확인하고, 버그를 발견하여 수정하는데 도움을 주는 역할을 한다.

     

    MockMVC를 이용한 Controller내의 흐름

    1. TestCase -> MockMVC

         TestCase내에서 MockMVC객체를 생성한다.

         이 객체는 테스트할 컨트롤러와 상호작용을 하는데 사용된다.

    2. MockMVC -> TestDispatcher Servlet

        MockMVC를 사용하여 원하는 엔드포인트에 요청을 보낸다.

        또한 해당 요청에 필요한 파라미터, 헤더 또는 쿠키 등을 설정한다.

        GET요청을 보낸다면, perform(MockMvcRequestBuilders.get("/endpoing"))와 같이 요청을 설정한다.

        파라미터 설정은 param("paramName", "paramValue")와 같이 파라미터를 설정할 수 있다.

    3. TestDispatcher Servlet -> Controller

        요청을 실행하고 응답을 받는다

         andExpect 메서드를 사용하여 응답의 상태코드, 헤더 , 본문 등을 검증할 수 있다.

    4. MockMVC -> TestCase

         필요한 검증을 추가한다.

         응답 본문의 내용을 검증하고 싶다면 andExpect(content().string("expectValue")) 와 같이 검증을 추가한다.

    https://terasolunaorg.github.io/guideline/5.4.1.RELEASE/en/UnitTest/ImplementsOfUnitTest /UsageOfLibraryForTest.html

     

     

     

    Example

    @ExtendWith(SpringExtension.class)
    @WebMvcTest
    public class MemberApiControllerTest {
        @Autowired
        private MockMvc mockMvc;
    
        @Autowired
        private ObjectMapper objectMapper;
    	
        ...
    
        @Test
        @DisplayName("회원가입 성공 테스트")
        public void successCreatePost() throws Exception {
    
            String content = objectMapper.writeValueAsString(memberDto);
    
            mockMvc.perform(post("/api/members")  	//"/api/members"로 GET 요청 수행
                    .content(content)				//
                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())		// 상태 코드 200인 성공 응답을 기대
                    .andDo(print());
        }

     

    MockMVC에서 사용되는 어노테이션

    어노테이션 설명
    @ExtendWith(MockitoExtension.class) Mockito를 사용하여 모킹하기 위해 테스트 클래스에 적용
    @WebMvcTest 웹 MVC테스트를 위한 스프링 컨텍스트를 구성
    @AutoConfigureJsonTesters JSON테스트를 위해 JsonTester의 자동 구성을 활성화
    @AutoConfigureMockMvc MockMvc를 자동으로 구성하는데 사용
    @Mock 모킹 대상 객체를 생성하여 주입
    @MockBean Spring 컨텍스트에서 Mock객체를 생성하여 주입
    @InjectMocks Mock Object(객체)를 생성하고 인스턴스를 주입하는데 사용
    @SpringBootTest 스프링 부트 애플리케이션의 통합 테스트를 위해 스프링 컨텍스트를 구성
    @BeforeEach 각각의 테스트 메서드가 실행되기 전에 실행되는 메서드 지정
    @Test 테스트 메서드 지정

     

     

    MockMVC 테스트 방식

    https://adjh54.tistory.com/347

    테스트 방식 설명
    Standalone 테스트 독립적으로 테스트를 수행
    다른 의존성 없이 테스트를 실행할 수 있으며, 외부 리소스나 서버에 대한 연결없이 테스트를 수행한다.
    Spring Context를 수행하여 테스트 Spring Context를 실행하여 테스트를 수행
    의존성 주입과 같은 Spring의 기능을 사용할 수 있게 해줌
    Web Server를 수행하여 테스트 Web Server를 실행하여 테스트를 수행
    실제 서버에 대한 테스트를 가능하게 해주며, 외부 리소스와의 상호작용을 테스트할 수 있다.

     

     

     

    정리

    mock()메서드, @Mock을 사용하게 되면 스프링 애플리케이션 컨텍스트를 로딩하지 않고도 사용할 수 있기에 단위 테스트 시에 유용하게 사용될 수 있다. 단위 테스트 시에 무분별하게 mocking을 통해 테스트를 작성하기 보다 외부 라이브러리와 같이 직접 제어할 수 없는 경우에 mocking을 사용하는 것이 좋다.

     

    @MockBean 은 스프링 애플리케이션 컨텍스트를 로딩해야 사용할수 있기 때문에, 스프링 애플리케이션 컨텍스트를 필요로 하는 통합 테스트에서 mocking이 필요할 때 유용하게 사용될 수 있다.

     

    MockMVC는 컨트롤러의 동작을 테스트하는 테스트용 라이브러리이다

     

     

     

     

    참고

     

    https://simgee.tistory.com/58

     

    https://brunch.co.kr/@springboot/207

     

    https://forkyy.tistory.com/62

     

    https://www.nextree.io/mockito/

     

    https://adjh54.tistory.com/347

     

    https://cobbybb.tistory.com/16

     

     

    728x90

    댓글

© 2022. code-space ALL RIGHTS RESERVED.