Spring Boot

[SPRING] 테스트코드 어노테이션, 라이브러리, 메서드 모음

재원쓰 2022. 12. 28. 18:07

포스트맨도 있는데 왜 굳이 테스트코드를 써야할까?

  1. 회원가입, 로그인을 뒤에 api 수행하는 일을 하지 않을 있다.
  2. 예외 상황(ex. 게시물 id 중복, 게시물 찾을수 없음, 올바른 전달 안함) 하나하나 테스트할 포스트맨에서 값을 바꿔가며 실행시켜볼 필요가 없다.
  3. 여러가지 예외 상황을 테스트코드에 정리할 있다.

어노테이션

@RunWith(SpringRunner.class)

Junit 실행할 때 스프링이랑 같이 엮어서 실행하겠다고 선언한다.

 

@SpringBootTest

스프링부트를 띄운 상태로 테스트한다. 이게 없으면 스프링의 애노테이션인 @Autowired 이런 것들이 다 실패한다.

 

@Transactional

DB에 추가/수정/삭제 등 변경될 때는 이걸 넣어준다. 테스트코드에서는 테스트가 마친 후 DB 변동 사항을 다 롤백 해준다. 

 

@Rollback(false)

DB 변동사항이 롤백 되는것을 원하지 않을 때 사용한다. 

 

@Test(expected = IllegalStateException.class)

테스트 메서드에 선언을 해준다. expected 속성을 이용하면 예외가 떳을 때 return 시킨다고 선언이 가능하다.

 

@ExtendWith(MockitoExtension.class)

JUnit과 Mockito를 연결해주는 어노테이션

 

@InjectMocks

본격적으로 테스트할 Sevice에 선언해준다. @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 Service에 주입시켜준다. 

주의할 점은 Service 쪽에 주입받은 것들(Repository, JwtUtil, PasswordEncoder 등)이 자동으로 끌고와지지 않기 때문에 하나하나 @Mock, @Spy로 다 불러와줘야 한다.

 

@Mock

실제 객체를 주입받는 것이 아니라 똑같이 사용이 가능한 가짜(Mock) 객체를 만들어 반환해준다.

예로 Repository에 선언해준다.

 

@Spy

테스트할 Service에서 호출되는 주입 메서드를 똑같이 사용 가능한 가짜(Mock) 객체로 만들어 사용할 것이라고 선언해준다. 예로 JwtUtil에 선언해준다.

 

@DisplayName("회원 가입")

여기에 적어놓은 이름으로 테스트할 때 돌아간다. 길게 쓰기도 한다. (ex. 회원 ID 중복으로 인한 가입 실패)

 

@BeforeEach

JUnit 메서드로, JUnit 생명주기 중각 테스트 메서드가 실행되기 전에 동작하는 메서드이다.  이외에도 @BeforeAll, @AfterAll, @AfterEach가 있다.

 


라이브러리, 메서드

new MockHttpServletRequest / new MockHttpServletResponse

Header에 세팅하거나 세팅된 값을 가져오는 기능을 도와주는 라이브러리로, 로그인 시 Header쪽에 세팅된 token 등의 값도 전달받아서 테스트를 돌려야 하는데, 이땐 MockHttpServletResponse를 사용한다.

 

MockMvc / MockMvcBuilder

Controller 계층에서는 Http 통신을 해야하는데 일반적인 방법으로는 할 수가 없다. 스프링에서 이런 점을 보완하기 위해 나온 라이브러리이다. Controller는 테스트가 돌기 전에 MockMvc를 사용해서 빌드를 한번 해주는 과정을 추가해준다.

 

new Gson().toJson(전달값)

API에 Http 통신 할 때 전달값을 Json으로 보내주기 위해 사용하는 라이브러리이다. import가 안된다면 gradle에 implementation을 추가해주어야 한다.

 

when( userRepository.findByUsername( any(String.class) ) ).thenReturn( Optional.empty() )

Mockito 메서드로, 가짜 객체로 받은 Repository 같은 것을 동작시키는 호출문이고 thenReturn쪽에 리턴받을 값을 설정해준다. 객체 필드에 임의의 값이 들어가 있는 상태로 객체를 반환할 수도 있고 위처럼 empty로 반환되게 할 수도 있다.

 

verify( userRepository, times(1) ).saveAndFlush( any(User.class) )

Mockito 메서드로, 로직 중에서 when 메서드에 동작을 지정해두지 않은 로직은 실제 수행이 되진 않기 때문에 이 코드까지 잘 거쳐지고 있는지를 확인이 필요한데, 이때 사용하는 메서드가 verify이다. 주로 repository.save() 로직 부분이 when으로 지정해주지 않기 때문에 이 부분에 사용한다. 'times(1)'는 해당 로직이 1번 거쳐진 게 맞는지 확인해준다.

 

assertThrows(IllegalArgmentException.class, () -> postService.getPost(0L))

예외가 내가 원하는대로 발생하는 것을 검증할 때 사용하는 JUnit 메서드이다. 첫번째 파라미터에 익셉션 종류, 두번째 파라미터에 실행시킬 로직을 넣는다.


알아둬야 할 부분

시큐리티가 적용된 로그인 시 Header 값으로 토큰이 잘 들어왔는지 검증

MockHttpServletResponse servletResponse = new HttpServletResponse();
 ... 로그인 테스트 ...
assertThat(servletResponse.getHeaderValue('Authorization').toString()).isNotEmpty();

토큰이 들어온 것을 테스트코드로 확인하는 방법

테스트코드의 맨 마지막 줄에 디버그 점을 찍고, 디버그 모드로 실행한다. 디버그 창에서 아래로 조금 내리면 headers를 펼쳐볼 수 있다.


기능 테스트 시 시큐리티가 적용된 회원가입, 로그인은 어떻게 거쳐야 할까?

기능을 테스트하는 데에 회원가입과 로그인 절차를 거쳐가야 한다는 생각은 '포스트맨 방식'만을 사용하면서 생긴 관념이다. 기능을 테스트할 때는 기능만 테스트 해야한다. 회원가입과 로그인을 다 패스하는 아주 쉬운 방법이 있다. 그냥 빈 객체를 생성해 넘기는 것이다.

@Test
public void createPost(){
    //given
    PostRequestDto requestDto = new PostRequestDto();
    User user = new User();

    //when
    postService.createPost(requestDto, user);
}

repository.findById를 테스트할 경우 id에 뭘 넘겨야 할까?

그건 의미가 없다. 어차피 가짜 Repository이기 때문에 어떤 값을 넣어도 찾을 객체가 없기 때문이다.

그럼 반환받은 객체를 어떻게 정의해야 할까? findById(Long id) 는 실제 Repository를 보면 Optional<Post>를 반환하고 있을텐데, 이렇게 반환

Optional 객체는 안에 객체가 없는 경우까지 고려하는 객체이기 때문에 Optional<Entity>로 받으면 상관이 없다.

//given
Post post = new Post("Hello");  // Post에 있는 content 필드에 임시로 "Hello" 값을 넣어준다.
when(postRepository.findbyId(anyLong())).thenReturn(Optional.of(post)) // postRepository.findbyId를 실행하면 content="Hello" 값이 있는 상태로 반환되게 설정한다.

//when
PostResponseDto response = postService.getPost(anyLong());

//then
assertThat(response.getContents()).isEqualTo(request.getContents())

Controller 쪽 테스트코드는 추가로 세팅할 게 있다?

Controller는 테스트 할 때 MockMvc를 사용해서 빌드를 한번 해줘야 한다. 각각의 테스트를 할 때마다 이전에 실행이 되어야 하기 때문에 @BeforeEach를 사용해준다.

@BeforeEach
public void init(){
    mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}