개발/고민과 생각

private method 까지 테스트 코드를 짜야할까

hojak99 2022. 11. 4. 02:40

먼저, 테스트 코드를 짜기 쉬운 코드가 있고 테스트 코드를 짜기 어려운 코드가 있을 것 같다.

내가 생각하는 테스트 코드를 짜기 쉬운 코드는 다음과 같다. 막상 생각하려니 잘 생각이 나질 않는다.

  1. SOLID 원칙을 적절히 지킨 코드
  2. 하나의 메소드가 여러 행위를 구현하지 않은 코드 (1의 연장선이겠지만)
  3. 길이가 짧은 메소드 (2의 연장선이겠지만)
  4. if, else 가 적은 메소드

만약 이런 코드가 존재한다면 어떻게 테스트 코드를 짜야할까?

fun register(request: Request) {
    checkValidPhone(request.phone)
    checkValidEmail(request.email)
    checkAgreeMarketing(request.isAgreeMarketing)
    
    val user = User(....request....)
    userRepository.save(user)
    
    val userTest = UserTest(....user.....)
    userTestRepository.save(userTest)
    return Result(user, userTest)
}

private fun checkValidPhone(phone): Boolean {
    val isValid = Regex phone...
    if(!isValid) {
    	throw NotValidPhone()
    } 
    return isValid
}

private fun checkValidEmail(email): Boolean {
    val isValid = Regex email...
    if(!isValid) {
    	throw NotValidEmail()
    } 
    return isValid
}

private fun checkAgreeMarketing(isAgreeMarketing) {
    if(isAgreeMarketing) {
    	// API 요청 코드 ...
    } 
}
 

딱 봐도 테스트 코드 짤려면 한 세월이다. 단위 테스트 케이스가 나오는 경우의 수를 계산해보자.

  1. 휴대폰 번호가 비정상일 때 exception 발생해야 한다.
  2. 이메일이 비정상일 때 exception 발생해야 한다.
  3. 마켓팅 수신 동의 시 특정 api 호출해야 한다.
  4. 마켓팅 수신 동의 시 특정 api 를 호출했는데 이 때 500 발생 시 ~ 해야한다.
  5. 마켓팅 수신 동의 시 특정 api 를 호출했는데 이 때 200 발생 시 ~ 해야한다.
  6. 마켓팅 수신 미동의 시 특정 api 호출하지 않아야 한다.
  7. 휴대폰 번호가 정상이면서 이메일이 정상일 경우 userRepository.save, userTestRepository.save 호출 1번씩 해야 한다.

꽤 귀찮아졌지만 여기서 더 귀찮은건 데이터를 매번 세팅해줘야한다는 것이다.

그러면 미리 데이터를 세팅하는 코드를 짜면 되지 않느냐? 할 수 있다. 이정도 코드면 데이터를 미리 세팅해도 좋다. 그렇지만 코드가 길어지고 하나의 public 메소드에 여러 세부 구현 사항이 존재하는 private method 들이 더욱 많아졌을 때도 미리 데이터를 세팅하는 코드를 짜면 될까? 난 아니라고 본다.

하나의 클래스에 필드가 20개라면? validation 체크하는 메소드에서 if 구절에 여러 개의 조건이 들어간다면? 테스트 코드는 결국 이렇게 된다.

fun register () {
   checkValid(...)
   ...
}

private fun checkValid(...) {
    if (birth > now() && email.length > 100 && phone.length > 11) {
      throw NotValid()
    }
    
    .....
}

// ....

Test {
    fun 생일이 현재 시간보다 클 때 exception 발생하는가? () {
        throw NotValid -> {
            User user = 유저 테스트 데이터()
            user.birth = 2023-03-03
            service.register(user)
        }      
    }
    
    fun 이메일이 100글자 초과하면 exception 발생하는가? () {
        throw NotValid -> {
            User user = 유저 테스트 데이터()
            user.birth = 2022-03-03
            user.email = ... 100글자 초과하게 만드는 코드
            service.register(user)
        }      
    }
    
    fun 휴대폰번호가 11글자 초과하면 exception 발생하는가? () {
        throw NotValid -> {
            User user = 유저 테스트 데이터()
            user.birth = 2022-03-03
            user.email = "valid@gmail.com"
            user.phonoe = .... 11 글자 초과하게 만드는 코드
            service.register(user)
        }      
    }
    
    fun 동작하는가() {
        User user = 유저 테스트 데이터()
        user.birth = 2022-03-03
        user.email = "valid@gmail.com"
        user.phonoe = "01011111111"
        service.register(user)    
    }
}

만약 private method 가 엄청나게 많은 필드에 대해 이런 validation 여부를 체크한다면 테스트 코드를 더 짜기 싫어질 뿐만 아니라 리펙토링으로 인하여 조건 순서가 변경된다면 기존에 저렇게 짜두었던 테스트 코드는 전체 다 조건 순서 변경으로 인해 수정해야 한다. 난 그저 회원가입 코드가 내가 의도한대로 잘 동작하는지 보고 싶었을 뿐인데.

다시 돌아와서 내가 그럼 저 register() 메소드에 대해서 뭘 테스트 하고 싶어했는가? 로 생각할 수 있다.

  1. 각 필드에 대해 validation 체크를 하는가? 
  2. 마켓팅 수신 동의 시 api 를 호출하는가?
  3. 데이터를 테이블에 넣도록 메소드를 잘 호출하는가?

난 이 3가지만 테스트를 하려고 했다. 그러면 어떻게 해야 하는가? 비즈니스 로직이 들어있거나 하는 private method 를 따로 클래스로 분리하면 된다. (대충 네이밍 짓겠다)

  • ValidRegisterService
  • CheckAgreeMarketingService

그리고 회원가입 시에는 해당 클래스의 메소드를 mocking 하면 된다. 그리고 각 클래스의 메소드를 호출하는가? 의 여부와 save 호출하는가? 만 짜면된다. 이 때 ValidRegisterService, CheckAgreeMarketingService 의 코드는 이미 테스트 코드가 작성되어 검증되어 있을 것이다라는 전제이다. 해당 클래스들에 대한 테스트 코드도 짜기 쉽다. 그냥 딱 email, birth, isAgreeMarketing 을 파라미터로 받는 메소드에 대해서만 짜면 되니까.


그렇지만 private method 를 클래스로 따로 빼기 힘든 경우가 있을 수 있다. 클래스로 분리하는 공수가 꽤 크다던지 리펙토링하기 어렵다던지...
그래서 먼저 테스트 코드를 작성하고 리펙토링하여 좀 더 사이드이펙트 없이 진행하려 했다.
그렇게 테스트 코드를 짜려고 보니 왠걸 중복되는 코드가 한 가득인 것이다. 처음에 10개정돈 잘 짤 수 있겠지만 그게 몇십개가 된다면 너무 귀찮을 것이다.

그래서 private method 를 protected, public 으로 바꾸고 mocking 시킨 다음에 따로 private method 에 대해 테스트 코드를 짜게된다면?
그러면 테스트 코드의 의미가 없어지게 된다고 생각한다. 왜냐면 리펙토링하려고 보니 이 private method 를 없애면 mocking 한 테스트 코드도 다 다시 짜야하고 private method 에 대한 테스트 코드도 다시 짜야하기에 그냥 에라 모르겠다 하고 리펙토링을 안해버릴 것 같다. 그래서 더 머리가 아프다. 테스트 코드를 쉽게 짜기 위해서는 구조를 바꿔야하는데 이 구조를 사이드 이펙트 최소화해서 바꾸기 위해선 테스트 코드가 있어야 하는데 이런 테스트 코드 조차도 짜기 어렵다면..?

그냥 열심히 짤 수 밖에.. 미리 더미 데이터를 만들어놓고 가져다 쓰던지.. 
하여튼 private method 에 대해선 최대한 안 짜는 방향으로..

마지막으로 하나 링크 남기고 간다.

https://shoulditestprivatemethods.com/

 

Should I Test Private Methods?

 

shoulditestprivatemethods.com

반응형