팀프로젝트/기타

테스트코드 작성하기

일일일코_장민기 2024. 4. 23. 20:29
728x90
TDD(테스트 주도 개발)을 정말 늦게 알았다.
선테스트 제작, 후 개발을 해야 편하다는 사실...
그래도 늦게 나마 테스트코드를 만들어 보았다.

사실 테스트코드를 배울 곳이 많지 않다 보니 맨땅 박치기 하면서 배운게 많았다.

테스트코드하면서 배운 사실들
1. dto의 annotation을 통해 많은 부분을 미리 제한할 수 있다.
package com.moonBam.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.ibatis.type.Alias;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

@Alias("MemberDTO")
@Getter
@Setter
@ToString
public class MemberDTO{
    
    @NotBlank
    @Email
    @Size(min = 4, max = 49, message="아이디는 최소 4글자 이상, 50글자 미만")
    private String userId;

    @NotBlank
    @Size(min = 6, max = 20, message="비밀번호는 최소 6글자 이상, 20글자 미만")
    private String userPw;

    @NotBlank
    @Size(min = 2, max = 49, message="닉네임은 최소 2글자 이상, 50글자 미만")
    private String nickname;

    @NotBlank
    private String secretCode ;
    
    private int googleConnected;
    
    private int naverConnected;
    
    private int kakaoConnected;
    
    @NotBlank
    private String userSignDate;
    
    @NotBlank
    private String role;

    @NotBlank
    private boolean enabled;
    
    public MemberDTO() {
       super();
    }
    
    public MemberDTO(String userId, String userPw, String nickname, String secretCode, int googleConnected,
          int naverConnected, int kakaoConnected, String userSignDate, String role, boolean enabled) {
       super();
       this.userId = userId;
       this.userPw = userPw;
       this.nickname = nickname;
       this.secretCode = secretCode;
       this.googleConnected = googleConnected;
       this.naverConnected = naverConnected;
       this.kakaoConnected = kakaoConnected;
       this.userSignDate = userSignDate;
       this.role = role;
       this.enabled = enabled;
    }
    
}

 

처음 만들었다 보니 이쁘지 않은 건 어쩔 수 없다.

2. dto는 하나를 돌려서 쓰기보다 사용되는 곳에 따라 지정하기

예를 들어 회원가입할 때 유저 아이디와 패스워드, 닉네임만 필요하다면 해당 사항만 따로 만드는 식이다.
다만, 내 회원가입에서는 빠지는게 거의 없다보니 그냥 했다.


3. 테스트코드는 내가 임시로 데이터를 넣어주고, DB에 넣지 않을 뿐이다.
테스트코드에서는 크게 5개로 나뉜다.
A. 테스트를 만들 함수에서 있을 수 있는 상황 선택(대충 return이 몇 개인지)
B. 테스트용 데이터 지정
C. return이 나뉠 때 함수를 사용하면 when을 사용해서 결과값 지정하기
D. MultiValueMap을 통해 params를 지정하기
E. perform / status / view().name() / attribute 지정하기

4. 테스트코드는 내가 데이터를 넣어준 것을 바탕으로 원본 코드를 돌린다.
예를 들어 원본 코드에 Sysout이 있다면 다 출력된다.

5. 성공 테스트를 짜고 실패 테스트를 짜는게 편하다
성공에서 조금씩 뭔가 빠지는게 실패 케이스이기 때문


원본 코드
package com.moonBam.controller.member;

import java.text.SimpleDateFormat;
import java.util.Date;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.moonBam.dto.MemberDTO;
import com.moonBam.service.member.LoginService;
import com.moonBam.service.member.RegisterService;

import jakarta.servlet.http.HttpServletRequest;


@Controller
public class RegisterController {

    @Autowired
    LoginService lServ;
    
    @Autowired
    RegisterService serv;
    
    @Autowired
    PasswordEncoder encoder;
    
    @Autowired
    MailController mc;
    
    @Autowired
    SecurityController sc;

    //회원가입
    @PostMapping("/RegisterData")
    public String InsertData(HttpServletRequest request, MemberDTO dto) throws Exception {
       
       System.out.println("RegisterData: "+ dto);

       String result = "member/Register/registerFailure";
       
       // 아이디 검증
       boolean isDuplicateID = serv.isUserIdDuplicate(dto.getUserId());
       if (isDuplicateID) { // 아이디 중복여부 재확인
          System.out.println("아이디 중복");
          request.setAttribute("mesg", "이미 가입된 아이디입니다. 확인해주세요");
          return result;
       }

       // 아이디 규격 통과
       System.out.println("아이디 확인");


       // 비밀번호 검증
       String userPw = dto.getUserPw();
       String userPwConfirm = request.getParameter("userPwConfirm");
       // 비밀번호와 비밀번호 재확인 번호 일치 확인
       if (!(userPw.equals(userPwConfirm))) {
          System.out.println("비밀번호 일치 오류 " + userPw + " " + userPwConfirm);
          request.setAttribute("mesg", "비밀번호가 일치하지 않습니다. 확인해주세요");
          return result;
       }


       // 닉네임 검증
       String nickname = dto.getNickname();
       boolean isDuplicateNickname = serv.isUserNicknameDuplicate(nickname);

       // 닉네임 중복 여부 확인
       if (isDuplicateNickname) {
          System.out.println("닉네임 중복");
          request.setAttribute("mesg", "이미 가입된 닉네임입니다. 확인해주세요");
          return result;
       }

       // 닉네임 규격 통과
       System.out.println("닉네임 확인");


       // 가입일 - 가입한 당일 날짜
       Date currentDate = new Date();
       SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
       String userSignDate = dateFormat.format(currentDate);


       //보안코드
       String secretCode = sc.encrypt(AnonymousBoardController.getNum(8));


       // 모든 규격을 통과한 경우, insert 진행
       dto.setUserPw(encoder.encode(userPw));
       dto.setSecretCode(sc.encrypt(secretCode));
       dto.setUserSignDate(userSignDate);
       int num = serv.insertNewMember(dto);

       // 성공적으로 insert된 경우, 회원가입 성공 페이지로 이동
       if (num == 1) {
          System.out.println("회원가입 성공");
          result = "member/Register/registerSuccess";
          mc.RegisterCompleteEmail(dto.getUserId(), dto.getNickname(), secretCode);

       // 모든 데이터가 규격을 통과했음에도 insert되지 않았을 경우, 회원가입 실패 페이지로 이동
       } else {
          System.out.println("회원가입 실패");
          request.setAttribute("mesg", "모종에 이유로 가입에 실패했습니다. 다시 한번 해주세요");
       }

       return result;
    }
}

 

테스트코드

package com.moonBam.controller.register;

import com.moonBam.dto.MemberDTO;
import org.junit.jupiter.api.BeforeEach;
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.boot.test.mock.mockito.MockBean;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import com.moonBam.controller.member.MailController;
import com.moonBam.controller.member.SecurityController;
import com.moonBam.service.member.LoginService;
import com.moonBam.service.member.RegisterService;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;


import java.text.SimpleDateFormat;
import java.util.Date;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.anyString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class RegisterControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    LoginService lServ;

    @MockBean
    RegisterService serv;

    @Autowired
    PasswordEncoder encoder;

    @MockBean
    MailController mc;

    @MockBean
    SecurityController sc;


    //각 테스트에서 사용되는 변수
    private MemberDTO mockDTO;
    private String test_userSignDate;

    @BeforeEach
    void setup() {
        //가입일 지정용 코드
        Date currentDate = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
        test_userSignDate = dateFormat.format(currentDate);

        //dto에 test데이터 입력
        mockDTO = new MemberDTO();
            mockDTO.setUserId("test_userID");
            mockDTO.setUserPw("test_userPw");
            mockDTO.setNickname("test_nickname");
            mockDTO.setSecretCode("test_secretCode");
            mockDTO.setUserSignDate(test_userSignDate);

        //test용 암호화PW 설정
        String testUserPw = encoder.encode("test_userPw");
        System.out.println("testUserPw = " + testUserPw);
    }

    @Test
    void success_test() throws Exception {

        //아이디 중복 검사 시, 중복 아님
        when(serv.isUserIdDuplicate(mockDTO.getUserId())).thenReturn(false);

        //닉네임 중복 검사 시, 중복 아님
        when(serv.isUserNicknameDuplicate(mockDTO.getNickname())).thenReturn(false);

        //회원가입 진행 후 update 데이터 수 1출력
        when(serv.insertNewMember(any(MemberDTO.class))).thenReturn((1));

        //회원가입 과정 필요한 모든 데이터를 map 형태로 형성
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
            formData.add("userId", "test_UserID");
            formData.add("userPw", "test_userPw");
            formData.add("userPwConfirm", "test_userPw");
            formData.add("nickname", "test_nickname");
            formData.add("secretCode", "test_secretCode");
            formData.add("userSignDate", test_userSignDate);

        // 실행
        mockMvc
                //RegisterData주소에 post로 전송
                .perform(post("/RegisterData")
                //사용될 데이터 지정
                .params(formData))
                //HTTP응답 상태 코드가 200인지 확인
                .andExpect(status().isOk())
                //응답 후의 이동 페이지가 어디로 가는지 지정
                .andExpect(view().name("member/Register/registerSuccess"))
                //모델에 mesg라는 속성이 없는지 확인(mesg가 없어야 성공적으로 가입)
                .andExpect(model().attributeDoesNotExist("mesg"));
    }


    @Test
    void fail_isUserIdDuplicate() throws Exception {

        //아이디 중복 검사 시, 아이디 중복
        //"existingUserId" / anyString() 기입
        when(serv.isUserIdDuplicate("existingUserID")).thenReturn(true);

        //회원가입 과정 필요한 모든 데이터를 map 형태로 형성
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
            formData.add("userId", "existingUserID");
            formData.add("userPw", "test_userPw");
            formData.add("userPwConfirm", "test_userPw");
            formData.add("nickname", "test_nickname");
            formData.add("secretCode", "test_secretCode");
            formData.add("userSignDate", test_userSignDate);

        // 실행
        mockMvc
                //RegisterData주소에 post로 전송
                .perform(post("/RegisterData")
                //사용될 데이터 지정
                .params(formData))
                //HTTP응답 상태 코드가 200인지 확인
                .andExpect(status().isOk())
                //응답 후의 이동 페이지가 어디로 가는지 지정
                .andExpect(view().name("member/Register/registerFailure"))
                //모델에 mesg라는 속성이 있는지 확인(mesg가 있어야 실패)(model / request에 따라 다름)
                .andExpect(request().attribute("mesg", "이미 가입된 아이디입니다. 확인해주세요"));

    }

    @Test
    void fail_notEqualPassword() throws Exception {
        //아이디 중복 검사 시, 중복 아님
        when(serv.isUserIdDuplicate(anyString())).thenReturn(false);

        //회원가입 과정 필요한 모든 데이터를 map 형태로 형성
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
            formData.add("userId", "test_UserID");
            formData.add("userPw", "test_userPw");
            formData.add("userPwConfirm", "notEqual_test_userPw");
            formData.add("nickname", "test_nickname");
            formData.add("secretCode", "test_secretCode");
            formData.add("userSignDate", test_userSignDate);

        // 실행
        mockMvc
                //RegisterData주소에 post로 전송
                .perform(post("/RegisterData")
                //사용될 데이터 지정
                .params(formData))
                //HTTP응답 상태 코드가 200인지 확인
                .andExpect(status().isOk())
                //응답 후의 이동 페이지가 어디로 가는지 지정
                .andExpect(view().name("member/Register/registerFailure"))
                //모델에 mesg라는 속성이 있는지 확인(mesg가 있어야 실패)
                .andExpect(request().attribute("mesg", "비밀번호가 일치하지 않습니다. 확인해주세요"));
    }

    @Test
    void fail_isDuplicateNickname() throws Exception {
        //아이디 중복 검사 시, 중복 아님
        when(serv.isUserIdDuplicate(anyString())).thenReturn(false);

        //닉네임 중복 검사 시, 중복 아님
        when(serv.isUserNicknameDuplicate(anyString())).thenReturn(true);

        //회원가입 과정 필요한 모든 데이터를 map 형태로 형성
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
            formData.add("userId", "test_UserID");
            formData.add("userPw", "test_userPw");
            formData.add("userPwConfirm", "test_userPw");
            formData.add("nickname", "existing_nickname");
            formData.add("secretCode", "test_secretCode");
            formData.add("userSignDate", test_userSignDate);

        // 실행
        mockMvc
                //RegisterData주소에 post로 전송
                .perform(post("/RegisterData")
                //사용될 데이터 지정
                .params(formData))
                //HTTP응답 상태 코드가 200인지 확인
                .andExpect(status().isOk())
                //응답 후의 이동 페이지가 어디로 가는지 지정
                .andExpect(view().name("member/Register/registerFailure"))
                //모델에 mesg라는 속성이 있는지 확인(mesg가 있어야 실패)
                .andExpect(request().attribute("mesg", "이미 가입된 닉네임입니다. 확인해주세요"));

    }

    @Test
    void fail_noReason() throws Exception {
        //아이디 중복 검사 시, 중복 아님
        when(serv.isUserIdDuplicate(mockDTO.getUserId())).thenReturn(false);

        //닉네임 중복 검사 시, 중복 아님
        when(serv.isUserNicknameDuplicate(mockDTO.getNickname())).thenReturn(false);

        //회원가입 진행 후 update 데이터 수 1출력
        when(serv.insertNewMember(any(MemberDTO.class))).thenReturn((0));

        //회원가입 과정 필요한 모든 데이터를 map 형태로 형성
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("userId", "test_UserID");
        formData.add("userPw", "test_userPw");
        formData.add("userPwConfirm", "test_userPw");
        formData.add("nickname", "test_nickname");
        formData.add("secretCode", "test_secretCode");
        formData.add("userSignDate", test_userSignDate);

        // 실행
        mockMvc
                //RegisterData주소에 post로 전송
                .perform(post("/RegisterData")
                        //사용될 데이터 지정
                        .params(formData))
                //HTTP응답 상태 코드가 200인지 확인
                .andExpect(status().isOk())
                //응답 후의 이동 페이지가 어디로 가는지 지정
                .andExpect(view().name("member/Register/registerFailure"))
                //모델에 mesg라는 속성이 있는지 확인(mesg가 있어야 실패)
                .andExpect(request().attribute("mesg", "모종에 이유로 가입에 실패했습니다. 다시 한번 해주세요"));

    }

}

 

@SpringBootTest         //테스트를 하기 위한 annotation
@AutoConfigureMockMvc   //자동으로 MockMvc를 구성하여 테스트에서 사용
@Transactional          //각각의 테스트 메서드가 실행될 때 트랜잭션을 시작하고, 테스트가 끝나면 롤백
@Autowired
private MockMvc mockMvc;    //MockMvc는 Spring MVC테스트를 수행하는데 사용(컨트롤러 행동 시뮬레이션)

@Autowired
    PasswordEncoder encoder; //SpringSecurityConfig에서 Bean등록

@MockBean
LoginService lServ;          //기존 코드에서 Autowired하던 class
@BeforeEach
void setup() {
   //각각의 테스트코드 실행 전에 시행될 코드 입력
}
@Test
void success_test() throws Exception {

    //when(method를 사용하는 상황).thenReturn(상황 종료 시 출력될 값)
    //아이디 중복 검사 시, 중복 아님
    when(serv.isUserIdDuplicate(mockDTO.getUserId())).thenReturn(false);

    //회원가입 과정 필요한 모든 데이터를 map 형태로 형성
    MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("userId", "test_UserID");

    // 실행
    mockMvc
            //RegisterData주소에 post로 전송
            .perform(post("/RegisterData")
            //사용될 데이터 지정(formData)
            .params(formData))
            //HTTP응답 상태 코드가 200인지 확인
            .andExpect(status().isOk())
            //응답 후의 이동 페이지가 어디로 가는지 지정(member/Register/registerSuccess로 전송)
            .andExpect(view().name("member/Register/registerSuccess"))
            //모델에 mesg라는 속성이 없는지 확인(mesg가 없어야 성공적으로 가입)
            .andExpect(model().attributeDoesNotExist("mesg"));
}
@Test
void fail_notEqualPassword() throws Exception {
    //아이디 중복 검사 시, 중복 아님
    when(serv.isUserIdDuplicate(anyString())).thenReturn(false);

    //회원가입 과정 필요한 모든 데이터를 map 형태로 형성
    MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        //코드 생략
        formData.add("userPw", "test_userPw");
        formData.add("userPwConfirm", "notEqual_test_userPw");

    // 실행
    mockMvc
            //코드 생략
            //응답 후의 이동 페이지가 어디로 가는지 지정(member/Register/registerFailure)
            .andExpect(view().name("member/Register/registerFailure"))
            //모델에 mesg라는 속성이 있는지 확인(mesg가 있어야 실패)(출력되는 메세지도 지정)
            .andExpect(request().attribute("mesg", "비밀번호가 일치하지 않습니다. 확인해주세요"));
}

 

성공 상황과 실패 상황을 요약해서 정리했다.
처음이 어렵지 짜다보면 할만하다.

'팀프로젝트 > 기타' 카테고리의 다른 글

Maven 프로젝트를 Gradle로 전환하기  (0) 2024.04.30
nGrider 사용  (0) 2024.04.22