테스트 코드 작성하기 !
이번에 제일 하고싶었던 것 중 하나이다. 시간에 쫓기면서 프로젝트를 진행하다보니 테스트코드를 제대로 작성한 적이 없어 테스트 코드를 제대로 작성해보고자 하였다.
단위별 테스트 진행은 오류를 줄여줄 수 있기에 꼭! 진행하는 것이 좋다고 들어서 해보고 싶었던 것이었다.
그래서 이번에 AdminController를 작성하며 함께 해보았다.
의존성 추가는 이전에 되어있었지만 제대로 쓰지 않아서 주석처리 해두고 있었는데 이제서야 제대로 역할을 할 수 있게 되었다. ☆*: .。. o(≧▽≦)o .。.:*☆
build.gradle 의존성 추가
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
🚨 문제 발생
Admin 페이지 접근 권한 주기
Admin페이지에 관리자만 접근 가능하게 접근 권한을 주었더니 401에러가 발생했다. 당연한 것...😶 (이걸로 접근 권한에 대한 테스트도 잘 된듯하다. 이 코드도 작성해야겠지? 놓칠 뻔 했는데 덕분에 생각이 났다.)
Status
Expected :200
Actual :401
<Click to see difference>
java.lang.AssertionError: Status expected:<200> but was:<401>
그래서 찾아보니 `@WithMockUser `라고 해서 가짜로 User를 만들어주는 테스트 어노테이션이 있었다.
`@WithMockUser` 의 역할에 ADMIN을 주입시켜 ADMIN으로 인식하도록 하여 접근이 가능하게 하였고 만약 접근한다면 get했을 때 OK할 수 있게 했다.
Member 생성 후 Body 결과값 확인
test 코드에서 Member를 생성해주는데 디버깅하면 잘 들어가고 있지만 print해서 출력결과를 아무리 살펴봐도 자꾸 Response가 아래와 같이 나왔다. data값도 안 들어가고 pageInfo에 정보도 이상하게 들어가고 있었다.
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = application/json
Body = {"data":[],"pageInfo":{"page":2,"size":10,"totalElements":12,"totalPage":2}}
Forwarded URL = null
Redirected URL = null
Cookies = []
나와야 하는 값은 다음과 같았다.
Body = {"data":[{"memberId":"1","email":"test1@test.com","name":"test1","profileImage":null,"point":0},{"memberId":"2","email":"test2@test.com","name":"test2","profileImage":"","point":20000}],"pageInfo":{"page":1,"size":10,"totalElements":2,"totalPage":1}}
AdminController를 다시 살펴보며 내가 작성에 있어 놓치고 있는 것이 있다는 것을 알았다. 여기서는 DTO로 변환을 시켜 출력을 해줬어야 했는데 그 과정을 거치지 않고 있었던 것이다.
🔻 AdminController.getMembers() 메서드
🔻 수정 전 testGetMembers() 메서드
@Test
@WithMockUser(roles = "ADMIN") // ADMIN 권한 가진 가짜 유저
public void testGetMembers() throws Exception {
int page = 1;
int size = 10;
List<Member> members = new ArrayList<>();
// 회원 1
Member member1 = new Member();
member1.setMemberId(1L);
member1.setName("test1");
member1.setEmail("test1@test.com");
member1.setPassword("password123@");
member1.setPhone("01011112222");
member1.setPoint(0L);
// 회원 2
Member member2 = new Member();
member2.setMemberId(2L);
member2.setName("test2");
member2.setEmail("test2@test.com");
member2.setPassword("password123@");
member2.setPhone("01033334444");
member2.setPoint(20000L);
members.add(member1);
members.add(member2);
// ✨ DTO 리스트 생성 추가 부분
// Page 객체 생성
Page<Member> memberPage = new PageImpl<>(members, PageRequest.of(page, size), members.size());
// Service 메서드 모킹
when(memberService.getMembers(page -1, size)).thenReturn(memberPage);
// 결과 생성
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/admin/members/info")
.param("page", "1")
.param("size", "10"))
.andExpect(status().isOk())
// ✨andExpect() 결과값 예상 메서드 추가
.andDo(MockMvcResultHandlers.print());
}
🔻 추가한 DTO 리스트 생성 부분
List<MemberAdminListResponseDto> memberDtos = members.stream()
.map(member -> {
MemberAdminListResponseDto dto = new MemberAdminListResponseDto();
dto.setMemberId(String.valueOf(member.getMemberId()));
dto.setEmail(member.getEmail());
dto.setName(member.getName());
dto.setProfileImage(member.getProfileImage());
dto.setPoint(member.getPoint());
return dto;
})
.collect(Collectors.toList());
🔻 추가한 andExpect() 결과값 예상 메서드
.andExpect(jsonPath("$.data", hasSize(members.size()))) // 데이터 리스트 크기 검증
.andExpect(jsonPath("$.data[0].memberId", is(member1.getMemberId().toString()))) // 첫 번째 멤버의 memberId 검증
Bean 생성 불가
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'adminController' defined in file [C:\Users\project\server\build\classes\java\main\com\codestates\server\domain\admin\controller\AdminController.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.codestates.server.domain.member.mapper.MemberMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Test를 돌렸는데 에러가 발생했다. bean이 생성되지 않아서 에러가 나고 있었다. 테스트 코드에서 MemberService만 사용되고 있어서 MemberService 클래스만 MockBean으로 등록해주었는데 그래서 에러가 난 것이다.
MemberMapper부터 사용되는 다른 관련 클래스들에 모두 MockBean 어노테이션을 추가 해서 MockBean 객체로 등록해주었더니 에러가 사라졌다!
최종 테스트 코드
ADMIN 접근 권한으로 접근 했을 때
✔️ 접근 권한 확인
✔️ 출력 결과 비교 (예상 출력결과와 동일한지)
✔️ 상태코드 비교 (200 OK 가 뜨는지)
@WebMvcTest(AdminController.class)
public class AdminControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MemberService memberService;
@MockBean
private MemberMapper memberMapper;
@MockBean
private BoardService boardService;
@MockBean
private AnswerService answerService;
@MockBean
private CommentService commentService;
@MockBean
private MemberRepository memberRepository;
@Test
@WithMockUser(roles = "ADMIN") // ADMIN 권한 가진 가짜 유저
@Transactional
@Rollback(value = false)
public void testGetMembers() throws Exception {
int page = 1;
int size = 10;
List<Member> members = new ArrayList<>();
// 회원 1
Member member1 = new Member();
member1.setMemberId(1L);
member1.setName("test1");
member1.setEmail("test1@test.com");
member1.setPassword("password123@");
member1.setPhone("01011112222");
member1.setPoint(0L);
// 회원 2
Member member2 = new Member();
member2.setMemberId(2L);
member2.setName("test2");
member2.setEmail("test2@test.com");
member2.setPassword("password123@");
member2.setPhone("01033334444");
member2.setProfileImage("");
member2.setPoint(20000L);
members.add(member1);
members.add(member2);
// DTO 리스트 생성
List<MemberAdminListResponseDto> memberDtos = members.stream()
.map(member -> {
MemberAdminListResponseDto dto = new MemberAdminListResponseDto();
dto.setMemberId(String.valueOf(member.getMemberId()));
dto.setEmail(member.getEmail());
dto.setName(member.getName());
dto.setProfileImage(member.getProfileImage());
dto.setPoint(member.getPoint());
return dto;
})
.collect(Collectors.toList());
// Page 객체 생성
Page<Member> memberPage = new PageImpl<>(members, PageRequest.of(page - 1, size), members.size());
// Service 메소드 모킹
when(memberService.getMembers(page - 1, size)).thenReturn(memberPage);
// Mapper 메소드 모킹
when(memberMapper.membersToMemberAdminListResponseDto(members)).thenReturn(memberDtos);
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/admin/members/info")
.param("page", String.valueOf(page))
.param("size", String.valueOf(size)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data", hasSize(members.size()))) // 데이터 리스트 크기 검증
.andExpect(jsonPath("$.data[0].memberId", is(member1.getMemberId().toString()))) // 첫 번째 멤버의 memberId 검증
.andDo(MockMvcResultHandlers.print());
}
✅ @Transactional 사용 이유
테스트 메서드에 트랜잭션을 적용함으로써 테스트 실행 시 수행되는 연산들이 하나로 묶일 수 있도록 하였다.
✅ Rollback사용 이유
테스트 진행 중 오류 진단을 위해 Rollback을 false하였다. 만약 true로 해놓는다면 테스트 종료 후 트랜잭션이 롤백되어 테스트 도중 진행된 내용들이 모두 Rollback되기 때문이다. 그래서 오류를 찾고 난 이후 Rollback 의 value값은 지워 default값인 true를 통해 test 시 항상 새로운 값을 사용할 수 있도록 하였다.
결과 확인
권한이 없는 사용자가 접근했을 때
✔️상태 코드 401 Unauthorized와 동일한지
권한이 없는 사용자가 접근할 때는 접근을 하는지 못 하는지만 알면 되기 때문에 멤버를 만들어주지는 않았다.
@Test
@Transactional
@Rollback(value = false)
public void testGetMembersUnauthorizedUser() throws Exception {
int page = 1;
int size = 10;
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/admin/members/info")
.param("page", String.valueOf(page))
.param("size", String.valueOf(size)))
.andExpect(status().isUnauthorized());
}
결과 확인
💬 느낀점
강의를 들을 때 말고는 테스트코드를 제대로 작성해 본 적이 없는 것 같은데 생각보다 차근차근 내가 작성해놓은 코드들을 살펴보면서 작성하면 어렵지 않다는 것을 알았다. (🔒 아직 import할 때 너무 헷갈리고 어떤 걸 써야할지 감이 안 와서 매번 찾아보느라 힘들었다.)
처음 작성하는 거라 테스트 코드를 짜는데 시간이 꽤 걸렸지만 계속 하다보면 이것도 금방 속도가 나지 않을까 하는 생각이 든다!
테스트 코드를 통해 더욱 완성도 있는 프로젝트가 되길 기대해본다~~
📖 참고자료
'FRAMEWORK > SPRING' 카테고리의 다른 글
[SPRING BOOT] 스프링부트 build 에러 `Execution failed for task ':test'.` 해결방법 (0) | 2024.06.27 |
---|---|
[Spring Boot] 스프링 부트 프로젝트 시작하기 / 프로젝트 생성하고 git 연동시키기 (1) | 2024.06.27 |
[SPRING] PROJECT 회원 관리 페이지 만들기 / spring boot 관리자 페이지 (2) | 2023.11.21 |
[SPRING] 작업 환경 분리하기 / 프로젝트 개발 환경, 배포 환경 profile 작업 (0) | 2023.11.15 |
[SPRING] 스프링 부트에서 의존성 못 찾을 때 `create class com.mysql.cj.jdbc.Driver` 에러 해결 방법 (0) | 2023.11.06 |