본문 바로가기

SpringBoot

Spring boot Junit 간단 사용 연습

개발하면서도 그렇지만 특히 기존소스를 리팩토링하면서 테스트코드의 필요성을 느꼈다.

이전에 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<StringString> map = new HashMap<StringString>();
        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값이 변경되어 제대로 작동하지 않는다. 


테스트코드 작성시 같은 테스트를 여러번 반복해도 같은 값이 나오도록 작성해야하고

매소드가 작동한다는 것을 검증하기 위해서 어떤값을 테스트 해야되는지 선정을 잘 해야 하는데 

이 부분이 생각처럼 쉽지는 않았다. 단순사용보다 자주 작성을 해보면서 연습이 필요할것 같다.