-
[SPRING] 테스트코드 어노테이션, 라이브러리, 메서드 모음Spring Boot 2022. 12. 28. 18:07
포스트맨도 있는데 왜 굳이 테스트코드를 써야할까?
- 회원가입, 로그인을 한 뒤에 api를 수행하는 일을 하지 않을 수 있다.
- 예외 상황(ex. 게시물 id 중복, 게시물 찾을수 없음, 올바른 값 전달 안함)를 하나하나 테스트할 때 매 번 포스트맨에서 값을 바꿔가며 실행시켜볼 필요가 없다.
- 여러가지 예외 상황을 테스트코드에 정리할 수 있다.
어노테이션
@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(); }
'Spring Boot' 카테고리의 다른 글
[SPRING] 서버 실행 시 더미데이터를 생성해보장! (0) 2023.01.02 [SPRING] AOP를 사용해보장 (0) 2023.01.01 ☑️[SPRING] 양방향 연관관계일 때 연관관계 편의 메서드를 만들어보장 (0) 2022.12.27 [SPRING] No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call; (0) 2022.12.26 [SPRING] 스프링시큐리티(Spring Security)를 사용해보장 (0) 2022.12.23