개발하면서도 그렇지만 특히 기존소스를 리팩토링하면서 테스트코드의 필요성을 느꼈다.
이전에 SpringBoot 1.5.x -> 2.0.x 버전업 이슈처럼 기존 코드를 리팩토링하면서 당연히 될거라고 생각했던 소스들이
문제되는 경우가 있었고 너무 당연(?)하게 될거라고 생각했기에 그런 부분에서 생긴 문제들을 발견하는게 어려웠다.
만약 테스트 코드가 존재했고 자동화된 테스트가 가능했다면
테스트를 통해 리팩토링시 문제가 기존 코드에서 나타난것인지 이전 코드에서 나타난것인지 발견하기 더 쉬웠을 것이다.
이전에 만들었던 GoogleOTP 프로젝트를 리팩토링하면서 연습해보았다.
예제 환경
- windows
- STS4
- SpringBoot 2.1
사용되는 기능
OTP는 크게 API를 통해 OTPKey를와 URL을 만드는기능 사용자가 입력한 임시 번호로 OTP인증을 하는기능으로 작동되며
위와같이 동작하기 위해 API에서 사용되는 CredentialRepositoryImpl 클래스 에서
유저 id로 key를 조회하는 메소드와 key생성시 유저 정보에 key값을 업데이트하는 메소드 구현 해야 한다.
원본 소스
CredentialRepositoryImpl - daoImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | package blue.coding.tistory.otp.service.impl; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.warrenstrange.googleauth.ICredentialRepository; import blue.coding.tistory.user.domain.User; import blue.coding.tistory.user.repository.UserRepository; @Service public class CredentialRepositoryImpl implements ICredentialRepository { @Autowired private UserRepository uRepo; /** * 사용자의 id를 입력하면 OTP key를 반환하는 매소드 * API내에서 호출되므로 유저는 직접 이 메소드를 호출할 필요가 없다 * @param userName 사용자 id, 사용자 이름이 아니다 * @return 유저정보에 저장된 key */ @Override public String getSecretKey(String userName){ User user= uRepo.findByUsername(userName); // 유저정보 조회 return user.getOtpSecretKey(); // 유저정보에서 key를 가져옴 } /** * 유저정보 DB에 Key값 저장 * API내에서 호출되므로 유저는 직접 이 메소드를 호출할 필요가 없다 * @param userName 유저id * @param secretKey OTP key */ @Override public void saveUserCredentials( String userName, String secretKey, int validationCode, List<Integer> scratchCodes) { //JPA에서 업데이트는 기존정보 + 수정할 정보를 save하는 형태로 사용 User user= uRepo.findByUsername(userName); // 아이디로 user정보 조회, user.setOtpSecretKey(secretKey); uRepo.save(user); // 유저정보에 Key 저장 } } | cs |
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | package blue.coding.tistory.otp; import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; import com.warrenstrange.googleauth.ICredentialRepository; import com.warrenstrange.googleauth.IGoogleAuthenticator; import blue.coding.tistory.user.domain.User; import lombok.extern.slf4j.Slf4j; @Slf4j @Controller public class OtpCrl { @Autowired private ICredentialRepository gService;//구글auth가 사용하는 서비스 @Autowired private IGoogleAuthenticator gAuth;//구글 API //============================페이지이동==================== //OTP 로그인 @GetMapping("/otpTest.do") public String otpTest() { return "otpTest"; } //OTP 등록 @GetMapping("/otpCheck.do") public String otpCheck() { return "otpCheck"; } //======================================================== /** * OTP Key와 URL를 등록하고 유저정보에 입력하는 기능 * @param resp * @param session * @param model * @return */ @GetMapping("/generateKeyUrl.do") public String generateKeyUrl(HttpSession session, Model model){ //API가 사용할 구현체 세팅 - 해당 인터페이스를 implements하여 2개의 필수 매소드를 구현한 상태여야함. gAuth.setCredentialRepository(gService); String ISSUER="GoogleAuthOTP_Exam"; //등록시 OTP 앱에서 보여지는 사이트명 User user=(User)session.getAttribute("userDto"); //Key생성 - 생성된 key는 CredentialRepositoryImple에서 구현한 saveUserCredentials메소드에 의해 자동으로 db에 저장됨 final GoogleAuthenticatorKey authKey = gAuth.createCredentials(user.getUsername()); //생성된 키를 가져옴 String key=authKey.getKey(); //QR코드생성 String qrCodeUrl //지정한 공급자명, 유저아이디 , 생성한key = GoogleAuthenticatorQRGenerator.getOtpAuthURL(ISSUER, user.getUsername(), authKey); user.setOtpSecretKey(key); //스코프에 key, url저장 Map<String, String> map = new HashMap<String, String>(); map.put("url", qrCodeUrl); map.put("encodedKey", key); model.addAttribute("urlKeyMap", map); //유저정보 세션에 다시저장-회원가입후 재로그인 하지않아도 세션에담긴 key를 가지고 OTP로그인 가능하게 하기위함 session.setAttribute("userDto", user); return "otpTest"; } //OTP로그인 기능 @GetMapping("/result.do") public String result(HttpServletResponse resp, HttpSession session,String user_code) throws IOException { User user=(User) session.getAttribute("userDto"); //공백제거 String user_input_code_noEmpty=user_code.trim().replaceAll("\\p{Z}", "");//정규 표현식 log.info("user_code :{}",user_code); //사용자가 입력한 OTP 임시 생성 비밀번호 일치여부 확인 boolean isCodeValid = gAuth.authorize(user.getOtpSecretKey(), Integer.parseInt(user_input_code_noEmpty)); log.info("코드 유효성검사 : {}", isCodeValid); //일치시 로그인 if(isCodeValid==true) { return "main";//true면 인증성공 }else{ return "main"; } } } | cs |
테스트코드
OtpControllerTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | package blue.coding.tistory.otp; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertThat; import org.junit.After; import org.junit.Before; 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.test.context.junit4.SpringRunner; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; import com.warrenstrange.googleauth.ICredentialRepository; import com.warrenstrange.googleauth.IGoogleAuthenticator; import blue.coding.tistory.user.domain.User; import blue.coding.tistory.user.service.impl.UserServiceImple; import lombok.extern.slf4j.Slf4j; @Slf4j //테스트를 사용하기위해 선언 @RunWith(SpringRunner.class) //기존의 spring-test에서 사용되는 @ContextConfiguration의 발전형 //ApplicationContext를 쉽게 생성하고 조작할수 있다. //classes 속성으로 특정 빈을 생성할수 있으며 설정하지 않을시 설정된 모든빈을 생성함 @SpringBootTest public class OtpCrlTest { @Autowired private ICredentialRepository gService;//구글auth가 사용하는 서비스 @Autowired private IGoogleAuthenticator gAuth;//구글 API @Autowired private UserServiceImple userServiceImple; User user = null; @Before // @Test가 실행되기전에 실행됨, 일반적으로 DTO세팅등 초기화 작업을 진행 public void before() { gAuth.setCredentialRepository(gService); // API에서 사용할 구현체 세팅 user = new User(); // key를 저장하기위한 회원정보 user.setUsername("BlueCoding"); user.setPlainPassword("pw"); } @After // @Test가 실행된 후에 실행됨, 테스트를 위해 설정된 값들을 초기화 하는데 사용 public void cleanup() { } @Test // 테스트할 코드 public void OTP키생성_QR코드생성_DB저장_테스트() { //given String ISSUER = "commanyName"; // OTP 등록시 노출되는 사이트정보 //when GoogleAuthenticatorKey authKey = gAuth.createCredentials(user.getUsername()); // Key생성, 저장 User userDBInfo = userServiceImple.userLogin(user); // 저장된 key 조회 String qrCodeUrl //지정한 공급자명, 유저아이디 , 생성한key = GoogleAuthenticatorQRGenerator.getOtpAuthURL(ISSUER, userDBInfo.getUsername(), authKey); // QR코드생성 log.info("getKey : "+authKey.getKey()); log.info("getDbKey : "+userDBInfo.getOtpSecretKey()); //then //문법에 따라 전자와 후자가 같은값인가? null인가? 같은 객체인가? 등 판단을 하기위해 사용 assertThat(authKey.getKey(), is(userDBInfo.getOtpSecretKey())); assertThat(qrCodeUrl, is(notNullValue())); // QR코드가 생성되었는지만 체크 } /** * otp인증기능 테스트 * OTP키생성_QR코드생성_DB저장_테스트 실행시 자동으로 DB에 key값이 업데이트 되므로 * 위에서 테스트한 유저와 다른유저로 테스트해야 한다 */ @Test public void OTP인증기능_테스트() { //given int tempCode = 668789; String userOtpSecretKey = "AWEFAGAWEWFWAEF"; //when boolean isCodeValid = gAuth.authorize(userOtpSecretKey , tempCode); //then assertThat(isCodeValid, is(true)); } } | cs |
UserServiceTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package blue.coding.tistory.user; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; 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.test.context.junit4.SpringRunner; import blue.coding.tistory.user.domain.User; import blue.coding.tistory.user.service.UserIService; import blue.coding.tistory.user.service.impl.UserServiceImple; @RunWith(SpringRunner.class) @SpringBootTest public class UserServiceTest { @Autowired private UserIService userIService; //회원가입 @Test public void 회원가입_로그인테스트() { //give User user = new User(); user.setUsername("BlueCoding"); user.setPlainPassword("pw"); //when userIService.userInsert(user); User userDBInfo= userIService.userLogin(user); //then assertThat(userDBInfo.getUsername(), is("BlueCoding") ); assertThat(userDBInfo.getPlainPassword(), is("pw") ); } //로그인 //회원정보조회 } | cs |
CredentialRepositoryImplTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | package blue.coding.tistory.otp; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import java.util.List; import org.junit.Before; 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.test.context.junit4.SpringRunner; import com.warrenstrange.googleauth.ICredentialRepository; import blue.coding.tistory.user.domain.User; import blue.coding.tistory.user.service.UserIService; @RunWith(SpringRunner.class) @SpringBootTest public class CredentialRepositoryImplTest { @Autowired ICredentialRepository cretentialRepository; @Autowired UserIService userIService; @Before public void setting() { //회원가입시 저장되는 유저정보 User user = new User(); user.setUsername("userID"); userIService.userInsert(user); } @Test public void key저장_key조회() { //given String userName = "userID"; String secretKey = "AWEFAGWEGHFWEWEF"; int validationCode = 0; List<Integer> scratchCodes = null; //when cretentialRepository.saveUserCredentials(userName, secretKey, validationCode, scratchCodes); // 회원 정보에 otpkey 업데이트 //then String key= cretentialRepository.getSecretKey(userName); assertThat(key, is(secretKey)); } } | cs |
이슈사항
OTP키를 생성 -> 사용자가 OTP를 핸드폰에 등록 -> 인증을 거쳐야되서 생성/인증을 모두 테스트하기 위해서는
같은 테스트 클래스를 2번 실행시켜야한다.
OTP key생성이 api에서 난수로 생성되기 때문에 위와같은 상황에서 하나의 유저로 테스트를 한다면
2번째 실행시 db에 저장되는 key값이 변경되므로
첫번째 메소드에서 otp key를 생성하고 두번째 메소드에서 첫번째 생성된 key로 otp 인증 테스트를 진행하면 key값이 변경되어 제대로 작동하지 않는다.
테스트코드 작성시 같은 테스트를 여러번 반복해도 같은 값이 나오도록 작성해야하고
매소드가 작동한다는 것을 검증하기 위해서 어떤값을 테스트 해야되는지 선정을 잘 해야 하는데
이 부분이 생각처럼 쉽지는 않았다. 단순사용보다 자주 작성을 해보면서 연습이 필요할것 같다.
'SpringBoot' 카테고리의 다른 글
스프링 시큐리티 기본인증 + Rest API 테스트 예제 (0) | 2018.12.26 |
---|---|
Spring Data JPA 2.0 에서 id Auto_increment 문제 해결 (0) | 2018.11.30 |