스프링 테스트 정리 노트

SionBackEnd·2022년 12월 29일
0

Spring(봄)

목록 보기
16/22

assertNotNull() : Null 여부 테스트

 assertNotNull(currencyName, "should be not null");

`assertNotNull()`_ 메서드를 사용하면 테스트 대상 객체가 null 이 아닌지를 테스트할 수 있습니다.

`assertNotNull()`_ 메서드의 첫 번째 파라미터는 테스트 대상 객체이고, 두 번째 파라미터는 테스트에 실패했을 때, 표시할 메시지 입니다.

assertThrows() : 예외(Exception) 테z스트

  assertThrows(NullPointerException.class, () -> getCryptoCurrency("XRP"));

`assertThrows()`의 첫 번째 파라미터에는 발생이 기대되는 예외 클래스를 입력하고, 두 번째 파라미터인 람다 표현식에서는 테스트 대상 메서드를 호출하면 됩니다.

테스트 케이스를 실행하면 getCryptoCurrency() 메서드가 호출되고, 파라미터로 전달한 “`XRP`”라는 키에 해당하는 암호 화폐가 있는지 map에서 찾습니다.

그런데 만약 [코드 3-184]에서 assertThrows()의 첫 번째 파라미터로 NullPointerException.class 대신에 IllegalStateException.class 으로 입력 값을 바꾸면 어떻게 될까요?
테스트 실행 결과는 “failed”입니다. 우선 기본적으로 IllegalStateException.classNullPointerException.class 은 다른 타입이고, IllegalStateException.classNullPointerException.class 의 상위 타입도 아니기 때문에 테스트 실행 결과는 “failed”입니다.
그렇다면 만약 NullPointerException.class 대신에 RuntimeException.class 또는 Exception.class 으로 입력 값을 바꾸면 이번에는 테스트 실행 결과가 어떻게 될까요?
이 경우, 테스트 실행 결과는 “passed”입니다.
NullPointerExceptionRuntimeException 을 상속하는 하위 타입이고, RuntimeExceptionException 을 상속하는 하위 타입입니다.
이처럼 assertThrows() 를 사용해서 예외를 테스트 하기 위해서는 예외 클래스의 상속 관계를 이해한 상태에서 테스트 실행 결과를 예상해야 된다는 사실을 기억하기 바랍니다.

API 계층

  • @SpringBootTest
    - @SpringBootTest 애너테이션은 Spring Boot 기반의 애플리케이션을 테스트 하기 위한 Application Context를 생성합니다.

    	  여러분들이 잘 알다시피 Application Context에는 애플리케이션에 필요한 Bean 객체들이 등록되어 있습니다.
  • @AutoConfigureMockMvc
    - 2. (2)의 @AutoConfigureMockMvc 애너테이션은 Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 해줍니다.
    여러분들이 Spring Boot의 자동 구성을 통해 애플리케이션의 설정을 손쉽게 사용하듯이 @AutoConfigureMockMvc 애너테이션을 추가함으로써 테스트에 필요한 애플리케이션의 구성이 자동으로 진행됩니다.
    (3)의 MockMvc 같은 기능을 사용하기 위해서는 @AutoConfigureMockMvc 애너테이션을 반드시 추가해 주어야 합니다.

package com.codestates.slice.controller.member;

import com.codestates.member.dto.MemberDto;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @Test
    void postMemberTest() throws Exception {
        // given  (1)
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
                                                        "홍길동",
                                                    "010-1234-5678");
        String content = gson.toJson(post); // (2)

        // when
        ResultActions actions =
                mockMvc.perform(                        // (3)
	                                post("/v11/members")  // (4)
                                        .accept(MediaType.APPLICATION_JSON) // (5)
                                        .contentType(MediaType.APPLICATION_JSON) // (6)
                                        .content(content)   // (7)
                                );

        // then
        MvcResult result = actions
                                .andExpect(status().isCreated()) // (8)
                                .andReturn();  // (9)

        // System.out.println(result.getResponse().getContentAsString());
    }
}

데이터 엑세스 계층

@DataJpaTest

@DataJpaTest 애너테이션을 테스트 클래스에 추가함으로써, MemberRepository의 기능을 정상적으로 사용하기 위한 Configuration을 Spring이 자동으로 해주게 됩니다.

@DataJpaTest 애너테이션은 @Transactional 애너테이션을 포함하고 있기 때문에 하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 처리 됩니다.

즉, 여러 개의 테스트 케이스를 한꺼번에 실행 시켜도 하나의 테스트 케이스가 종료될 때마다 데이터베이스의 상태가 초기 상태를 유지한다는 것입니다.

package com.codestates.slice.repository.member;

import com.codestates.member.entity.Member;
import com.codestates.member.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest   // (1)
public class MemberRepositoryTest {
    @Autowired
    private MemberRepository memberRepository;   // (2)

    @Test
    public void saveMemberTest() {
        // given  (3)
        Member member = new Member();
        member.setEmail("hgd@gmail.com");
        member.setName("홍길동");
        member.setPhone("010-1111-2222");

        // when  (4)
        Member savedMember = memberRepository.save(member);

        // then  (5)
        assertNotNull(savedMember); // (5-1)
        assertTrue(member.getEmail().equals(savedMember.getEmail()));
        assertTrue(member.getName().equals(savedMember.getName()));
        assertTrue(member.getPhone().equals(savedMember.getPhone()));
    }
}

데이터 베이스 초기화

데이터 베이스를 초기화 해주어야지 테스트를 진행할때 id값의 구애 받지 않는다.

실패 케이스 먼저 테스트를 주로 진행한다.

injectionMocks
mock

injectMock 에너테이션이붙은 객체로 mock 객체들의 의존성이 주입된다. injectionMocks에너테이션이 붙은 객체는 heap메모리에 올라가고 나머진 가짜 객체가 주입된다.

2. Stub로 결과 처리

앞서 설명하였듯, 의존성이 있는 객체는 가짜 객체(Mock Object)를 주입하여 어떤 결과를 반환하라고 정해진 답변을 준비시켜야 한다. Mockito에서는 다음과 같은 stub 메소드를 제공한다.

  • doReturn(): Mock 객체가 특정한 값을 반환해야 하는 경우
  • doNothing(): Mock 객체가 아무 것도 반환하지 않는 경우(void)
  • doThrow(): Mock 객체가 예외를 발생시키는 경우
이제 의존성 주입을 해주어야 한다. 먼저 테스트 대상인 UserController에는 가짜 객체 주입을 위한 @InjectMocks를 붙여주어야 한다. 그리고 UserService에는 가짜 Mock 객체 생성을 위해 @Mock 어노테이션을 붙여주면 된다.

컨트롤러를 테스트하기 위해서는 HTTP 호출이 필요하다. 일반적인 방법으로는 HTTP 호출이 불가능하므로 스프링에서는 이를 위한 MockMVC를 제공하고 있다. MockMvc는 다음과 같이 생성할 수 있다.
//verify 
verify(userRepository, times(1)).save(any(User.class)); 
verify(passwordEncoder, times(1)).encode(any(String.class));

실제 서버에서는 id로 값을 확인하는 테스트는 지양하자

https://www.youtube.com/watch?v=YZVn76Rg-wI&list=PL93mKxaRDidEZfpXoyWZ-2ZLsYrQByDMP&index=13
컨트롤러는 Entity를 볼 필요가 없다. 그러므로 트랜잭션을 서비스로직 단에서 끊어주고 컨트롤러에게는 dto를 보내주는 것이 맞다. 모든 서비스는 서비스 로직에서 실행되어야하니까
![[스크린샷 2022-11-04 오후 4.01.37.png]]![[스크린샷 2022-11-04 오후 4.01.51.png]]

given과 when 의 차이점
Mockito와 BDDMockito의 차이점이다. 원래는 given만 사용했었지만, BDD에서 나온 given, when, then 메서드로 순서에 맞쳐서 값을 넣을 수 있게 만든것이다.

Assertions.assertThat () 앞쪽이 실제 나올 코드 뒤쪽이 예상하는 코드

open-in-view 기본값이 true 인데 false로 yml에 작성해야한다.
컨트롤러단까지 (view) 트랜잭션을 허용하는것

컨트롤러

서비스 로직에서 터지면 뭐할래
성공하면 뭐할래
dto에서 터지면 뭐할래

서비스로직
서비스 로직 내부의 에외가 터지면 뭐할래
여러개가 될 수도 있다.

예외 클래스를 처리해야지 컨트롤러의 테스트를 진행할 수 있다.
그것이 tdd의 장점

then의 Assertions를 진행할때 기대값은 항상 상수를 집어넣는다.

mockMvc

그러면 이제 mockMvc가 Null인지를 검사하는 테스트는 더 이상 필요가 없다. 그러므로 이 테스트는 제거하고 init 함수만 남긴 채로 API 개발에 들어가도록 하자. 참고로 컨트롤러 테스트를 위해 @WebMvcTest를 이용할수도 있다. 하지만 @WebMvcTest를 이용하면 테스트 속도가 느리므로 직접 MockMvc를 만들어주도록 하자.
@ExtendWith(MockitoExtension.class) 
public class MembershipControllerTest { 

	@InjectMocks 
	private MembershipController target; 
	private MockMvc mockMvc; 
	private Gson gson; 
	
	@BeforeEach public void init() { 
	mockMvc = MockMvcBuilders.standaloneSetup(target) .build(); } 
	
	@Test public void mockMvc가Null이아님() throws Exception { 
    assertThat(target).isNotNull(); assertThat(mockMvc).isNotNull(); } }

@WebMvcTest

위와 같이 MockMvc를 생성하는 등의 작업은 번거롭다. 다행히도 SpringBoot는 컨트롤러 테스트를 위한 @WebMvcTest 어노테이션을 제공하고 있다. 이를 이용하면 MockMvc 객체가 자동으로 생성될 뿐만 아니라 ControllerAdvice나 Filter, Interceptor 등 웹 계층 테스트에 필요한 요소들을 모두 빈으로 등록해 스프링 컨텍스트 환경을 구성한다. @WebMvcTest는 스프링부트가 제공하는 테스트 환경이므로 @Mock과 @Spy 대신 각각 @MockBean과 @SpyBean을 사용해주어야 한다.

하지만 여기서 주의할 점이 있다. 스프링은 내부적으로 스프링 컨텍스트를 캐싱해두고 동일한 테스트 환경이라면 재사용한다. 그런데 특정 컨트롤러만을 빈으로 만들고 @MockBean과 @SpyBean으로 빈을 모킹하는 @WebMvcTest는 캐싱의 효과를 거의 얻지 못하고 새로운 컨텍스트의 생성을 필요로 한다. 그러므로 빠른 테스트를 원한다면 직접 MockMvc를 생성했던 처음의 방법을 사용하는 것이 좋을 수 있다.

컨트롤러 테스트시 꼭 필요한 에너테이션

  • @WebMvcTest(실제 주입시킬 클래스)
  • @Autowired (MockMvc)
  • @MockBean(주입시킨 클래스에 주입되어있는 객체들)

컨트롤러 테스트시 꼭 이해해야 하는 메소드들

ResultActions / perform / accept / contentType / content / andExpect (jsonPath) (status)

ResultActions perform = mockMvc.perform(  
        post("/users")  
                .accept(MediaType.APPLICATION_JSON)  
                .contentType(MediaType.APPLICATION_JSON)  
                .content(content)  
);  
perform.andExpect(status().isCreated())  
        .andExpect(jsonPath("$.email").value(users.getEmail()))  
        .andExpect(jsonPath("$.nickname").value(users.getNickname()))  
        .andExpect(jsonPath("$.password").value(users.getPassword()));

@ParameterizedTest

@MethodSource

우리가 작성한 멤버십 등록이 실패하는 다음의 3가지 테스트는 코드가 상당히 유사하고 파라미터만 다르다. 즉, 중복인 것이다.

Junit5에서는 동일한 테스트 케이스에 대해 파라미터를 다르게 실행할 수 있는 기능을 @ParameterizedTest를 통해서 제공한다. 그러므로 @ParameterizedTest를 통해서 3가지 케이스를 1개의 테스트로 만들고 파라미터만 다르게 하여 중복을 제거하도록 하자.

먼저 @Test대신 @ParameterizedTest 어노테이션을 붙여주고 @MethodSource로 파라미터를 작성한 함수 이름을 적어주면 된다. 그리고 아래와 같이 파라미터를 넘겨주는 함수를 작성하고 테스트를 실행해보면 테스트가 통과함을 볼 수 있다.

@ParameterizedTest 
@MethodSource("invalidMembershipAddParameter") 
public void 멤버십등록실패_잘못된파라미터(final Integer point, final MembershipType membershipType) throws Exception { 
// given 
final String url = "/api/v1/memberships"; 
// when 
final ResultActions resultActions = mockMvc.perform( 
	MockMvcRequestBuilders.post(url) 
		.header(USER_ID_HEADER, "12345") 
		.content(gson.toJson(membershipRequest(point, membershipType))) 
		.contentType(MediaType.APPLICATION_JSON) 
	); 
	
	// then 
	resultActions.andExpect(status().isBadRequest()); 
} 
	
	private static Stream<Arguments> invalidMembershipAddParameter() { 
	
	return Stream.of( 
		Arguments.of(null, MembershipType.NAVER), 
		Arguments.of(-1, MembershipType.NAVER), 
		Arguments.of(10000, null) 
	); 
}

상수화 하여 테스트 클래스 필드값으로 사용하자.
메소드명은 상세하게
디티오 명은 Request+행동 , Response + 행동
디티오에는 절대로 엔티티그대로 들어가면 안된다. 반드시 String Inteager, Long, List<> 등으로 값을 분출해야한다.

리스트 테스트

리스트를 테스트 할때에는 is메소드를 사용한다. is()하고 스태틱 메서드를 불러오면
![[스크린샷 2022-11-06 오후 9.41.42.png]]
위와 같은 메서드를 임포트 하고 ()안에는제이슨 경로에 들어있어야 할 예상 리스트를 집어넣는다.

.andExpect(jsonPath("$.userRole", is(responseLongin.getUserRole())));
profile
많은 도움 얻어가시길 바랍니다!

0개의 댓글