구글, 네이버 로그인 구현을 완료했고
기존 테스트에 시큐리티 적용으로
문제가 되는 것을 해결한다.
기존 테스트 코드에서는
API를 바로 호출하도록 구성되어 있었다.
그러나 시큐리티 옵션이 활성화되면
인증된 사용자만이 API를 호출할 수 있다.
기존 API 테스트 코드들은 인증에 대한 권한을 받지 못해
테스트 코드마다 인증한 사용자가 호출한 것처럼
작동할 수 있도록 수정해야 한다.
우선 전체 테스트를 수행해본다.
전체 테스트 수행하기
인텔리제이 화면 우측의
Gradle 탭을 누른 뒤
Tasks - verification - test를 차례로 선택한 뒤
전체 테스트를 수행한다.
전체 테스트를 수행하면 위와 같은 결과가 나온다.
문제 1. Test 환경 구성
src/main과 src/test의 환경은
독자적인 환경 구성을 갖는다.
src/main/resources/application.properties가
테스트 코드를 수행할 때에도 적용되는데
test에 application.properties가 없는 경우
main의 설정을 그대로 반영한다.
그러나, 자동으로 반영하는 범위는
application.properties까지이고
application.oauth.properties는 test에 파일이 없어도
가져오는 파일이 아니다.
이 문제를 해결하기 위해 테스트 환경을 위한
application.properties를 생성한다.
그리고 설정 값들은 테스트 용으로 설정한다.
spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.h2.console.enabled=true
spring.session.store-type=jdbc
# Test OAuth
spring.security.oauth2.client.registration.google.client-id=test
spring.security.oauth2.client.registration.google.client-secret=test
spring.security.oauth2.client.registration.google.scope=profile,email
문제 2. 302 Status Code
registerd_Posts의 로그를 확인하면
다음과 같은 실패 로그를 볼 수 있다.
302 Status Code
200(정상 응답)을 원했지만
실제로 302(redirection 응답)가 도착해 실패했다.
이는 스프링 시큐리티 설정 때문에
인증되지 않은 사용자의 요청을 이동시키기 때문이다.
이런 API 요청은
임의로 인증된 사용자를 추가해
API만 테스트할 수 있게 설정한다.
스프링 시큐리티 테스트를 위한
여러 도구를 지원하는 spring-security-test를
build.gradle에 추가해 준다.
build.gradle
testCompile('org.springframework.security:spring-security-test')
그리고 PostsApiControllerTest의
2개의 테스트 메소드에
임의 사용자 인증을 추가한다.
PostsApiControllerTest.java
@Test
@WithMockUser(roles = "USER")
public void registered_Posts() throws Exception{
// 생략
}
@Test
@WithMockUser(roles = "USER")
public void update_Posts() throws Exception{
// 생략
}
@WithMockUser(roles = "USER")
인증된 임의의 사용자를 만들어 사용.
rolse = ""에 권한을 추가할 수 있다.
여기까지 코드를 작성했을 때
테스트 코드는 아직 작동하지 않는다.
권한을 주는 어노테이션이 MockMvc에서만 작동하기 때문이다.
PostsApiController는 @SpringBootTest로 되어 있고
MockMvc를 사용하지 않는다.
MockMvc를 사용하기 위해
코드를 수정해 준다.
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.springframework.http.MediaType;
import com.mieumje.springboot.domain.posts.Posts;
import com.mieumje.springboot.domain.posts.PostsRepository;
import com.mieumje.springboot.web.dto.PostsSaveRequestDto;
import com.mieumje.springboot.web.dto.PostsUpdateRequestDto;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@After
public void tearDown() throws Exception{
postsRepository.deleteAll();
}
// 등록 기능
@Test
@WithMockUser(roles = "USER")
public void registered_Posts() throws Exception{
//given
String title = "title";
String content = "content";
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.author("author")
.build();
String url = "http://localhost:" + port + "/api/v1/posts";
//when
//ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(new ObjectMapper().writeValueAsString(requestDto)))
.andExpect(status().isOk());
//then
//assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
//assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
// 수정/조회 기능
@Test
@WithMockUser(roles = "USER")
public void update_Posts() throws Exception{
//given
Posts savedPosts = postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updateId = savedPosts.getId();
String expectedTitle = "title2";
String expectedContent = "content2";
PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
.title(expectedTitle)
.content(expectedContent)
.build();
String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;
HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
//when
//ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT,requestEntity,Long.class);
mvc.perform(put(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(new ObjectMapper().writeValueAsString(requestDto)))
.andExpect(status().isOk());
//then
//assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
//assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
@Before
테스트가 시작되기 전에
MockMvc 인스턴스를 생성.
mvc.perform
생성된 MockMvc를 통해 API를 테스트.
그리고 다시 Gradle로 테스트를 수행한다.
Posts 테스트도
테스트를 통과한 것을 볼 수 있다.
문제 3. @WebMvcTest에서 CustomOAuth2UserService를 찾을 수 없다.
return_hello 테스트를 확인해보면
No qualifying bean of type 'com.mieumje.springboot.config.auth.CustomOAuth2UserService
메시지가 나오는 것을 확인할 수 있다.
문제 1에서 스프링 시큐리티 설정은 했지만
@WebMvcTest는 CustomOAuth2UserService를
스캔하지 않아 발생하는 메시지이다.
@WebMvcTest는
@ControllerAdvice, @Controller를 읽고
@Repository, @Service, @Component는 스캔 대상이 아니다.
SecurityConfig는 읽지만,
SecufityConfig를 생성하기 위한
CustomOAuth2UserService를 읽을 수 없어 에러가 발생하는 것이다.
이 문제를 해결하기 위해
스캔 대상에서 SecurityConfig를 제거해 준다.
HelloControllerTest.java
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)
}
)
그리고 마찬가지로
@WithMockUser(roles = "USER")를 통해
가짜로 인증된 사용자를 추가한다.
public class HelloControllerTest {
@Autowired
private MockMvc mvc;
@Test
@WithMockUser(roles = "USER")
public void return_hello() throws Exception{
// 생략
}
@Test
@WithMockUser(roles = "USER")
public void return_hello_dto() throws Exception{
//생략
}
}
다시 테스트를 진행하면
다음과 같은 추가 에러가 발생한다.
@EnableJpaAuditing으로 인해 발생한 것인데
이를 사용하기 위해서는
최소한 하나의 @Entity 클래스가 필요하다.
@WebMvcTest라서 @Entity 클래스가 없는 것이다.
@EnableJpaAuditing가 @SpringBootApplication과 함께 있어
@WebMvcTest에서도 스캔하게 되었다.
그래서 이 둘을 분리해주도록 한다.
Application.java에서
@EnableJpaAuditing을 제거한다.
Application.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
// @EnableJpaAuditing //JPA Auditing 제거
@SpringBootApplication
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}
}
그리고 confing 패키지에
JpaConfig를 생서해
@EnableJpaAuditing을 추가해 준다.
JpaConfig.java 위치
JpaConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing // JPA Auditing 활성화, Application.java에서 활성화 하던 것을 옮김
public class JpaConfig {
}
그리고 다시 전체 테스트를 진행하면
모든 테스트가 통과된 것을 볼 수 있다.
'프로그래밍 > OOP_Pattern_TDD' 카테고리의 다른 글
TDD - Junit4, Junit5 예외 테스트 방법 (0) | 2022.08.24 |
---|---|
[TDD]Springboot + Gradle + Jacoco 커버리지 확인 (0) | 2022.08.09 |
TDD 기능 명세, 설계 (0) | 2022.08.05 |
TDD - Gradle 프로젝트에 JaCoCo & SonarQube 적용 (0) | 2022.08.05 |
TDD 테스트 코드 작성 순서 (0) | 2022.08.03 |