본문 바로가기

Spring/Ticketing 프로젝트

테스트 코드 개선하기

반응형

개요

  • Flab 티케팅 프로젝트를 진행하며 받았던 테스트 코드에 대한 피드백과 개선사항을 정리하고자 합니다.
  • 티케팅 프로젝트에서 테스트시 사용하는 프레임워크와 라이브러리는 아래와 같습니다.
    • Kotlin
    • Kotest(+ kotest-extensions-spring)
    • mockk

개선 전 UnitTest

  • 저는 모든 TestCode를 BDD형식으로 작성하고 개발하고 있었습니다. 아래는 이에 대한 예시코드로, UserService의 회원가입 로직을 검증하는 테스트입니다.
given("이메일 인증 코드 검증이 완료된 사용자의 경우") {
            val email = "email@email.com"

            every { emailVerifier.checkVerified(email) } returns Unit

            `when`("정상적인 추가 개인 정보를 입력하여 회원가입을 완료할 시") {
                val userPW = "abc1234!"
                val userPWConfirm = "abc1234!"
                val nickname = "minturtle"

                val encrypteduserPW = "asldll321lslafas231412@3@!Ffa"
                val uid = "123asf"

                val dto = UserRegisterDto(
                    email,
                    userPW,
                    userPWConfirm,
                    nickname
                )

                val expectedUser = User(uid, email, encrypteduserPW, nickname)

                every { userPWEncoder.encode(userPW) } returns encrypteduserPW
                every { nanoIdGenerator.createNanoId() } returns uid
                every { userRepository.save(any()) } returns expectedUser

                userService.saveVerifiedUserInfo(dto)

                then("DB에 회원 정보를 추가해 저장할 수 있다.") {
                    verify { userRepository.save(expectedUser) }
                }
            }
        }
  • 그런데 멘토님께서 아래와 같은 피드백을 주셨습니다.
BehaviorSpec으로 작성한 테스트 코드는 Depth가 길어져서 보기 쉽지 않습니다. 그래서 유닛테스트에서 기능이 명확하게 잘 안드러나는 것 같습니다.
  • 이러한 피드백을 받고 테스트 코드를 한번 쭉 훑어 봤는데, 확실히 아래의 문제점이 있었습니다.
    • 멘토님께서 말씀하신대로 Depth가 너무 깊어집니다.
    • 사용자의 행위가 Given이 아닌 When에 드러나기 때문에 어떠한 기능에 대한 테스트인지 한눈에 들어오지 않습니다.
    • 또한 기능 중심이 아닌 사용자 행위 중심의 테스트이기 때문에 마찬가지로 어떤 기능에 대한 테스트인지 파악하기 쉽지 않습니다.

Unit Test 개선하기

  • 그래서 테스트 코드를 어떻게 개선할 수 있을까?에 대해 구글링을 하던 중, 카카오에서 찍은 영상하나를 볼 수 있었습니다.
  • https://tv.kakao.com/channel/3693125/cliplink/414004682
  • 위 동영상에서 가장 와닿았던 내용은 총 두가지였습니다.
    • BDD와 TDD의 차이점, BDD는 사용자의 행위 중심, TDD는 기능 중심
    • BDD의 하나의 큰 싸이클에 TDD를 포함시켜야 합니다. 즉 BDD 중심의 통합 테스트를 시작으로 탑다운으로 내려가며 TDD로 UnitTest를 작성해야 합니다.
    • → 즉 BDD와 TDD는 상호보완적인 것으로, BDD는 통합 테스트에, TDD는 단위 테스트에 적용하는 것이 적합해 보입니다.
  • 그래서 기존에 BehaviorTest로 작성되었던 단위 테스트를 간결하게 바꾸기 위해 StringSpec으로 변경하기로 했습니다.
  • 또 기존의 개발 방식은 먼저 UnitTest를 작성해 개발을 완료한 후 코드 리뷰를 받고 통합테스트를 작성하는 식으로 개발하였습니다.
  • 근데 앞서 언급했듯 BDD 싸이클에 TDD를 포함시키기 위해 먼저 통합테스트를 작성하고, Top-Down 방식으로 내려가며 TDD를 적용하여 개발하기로 했습니다.
  • 다음으로 고민해야 했던 것은 그럼 BDD가 아닌 TDD에서는 어떻게 네이밍을 해야 기능이 잘 드러날까? 였습니다.
    • 이에 대해 잘 정리된 블로그 글을 볼 수 있었는데, 아마 비슷한 내용의 글이 많은걸로 봐서 누군가가 적어 놓은걸 번역해서 가져온 것 같습니다. https://it-is-mine.tistory.com/3
  • 위 블로그 글을 참조하며, 7가지 네이밍 기법 중 개인적으로는 4번 네이밍 기법이 가장 기능을 나타내기에 좋아보였습니다.
테스트할 기능
테스트 메서드를 식별하는 방법으로 주석을 사용하고 있기 때문에, 테스트할 기능만 간단하게 쓰는 것이 더 낫다는 의견이 많습니다. 또한 코드의 악취를 방지하고, 문서화된 형태의 유닛 테스트를 수행하므로 권장되는 방법입니다.
예시
- IsNotAnAdultIfAgeLessThan18
- FailToWithdrawMoneyIfAccountIsInvalid
- StudentIsNotAdmittedIfMandatoryFieldsAreMissing

 

개선 후 UnitTest

  • 그래서 StringSpec을 적용해 테스트 코드를 개선하면 아래와 같은 형태가 됩니다.

 

Before

given("이메일 인증 코드 검증이 완료된 사용자의 경우") {
            val email = "email@email.com"

            every { emailVerifier.checkVerified(email) } returns Unit

            `when`("정상적인 추가 개인 정보를 입력하여 회원가입을 완료할 시") {
                val userPW = "abc1234!"
                val userPWConfirm = "abc1234!"
                val nickname = "minturtle"

                val encrypteduserPW = "asldll321lslafas231412@3@!Ffa"
                val uid = "123asf"

                val dto = UserRegisterDto(
                    email,
                    userPW,
                    userPWConfirm,
                    nickname
                )

                val expectedUser = User(uid, email, encrypteduserPW, nickname)

                every { userPWEncoder.encode(userPW) } returns encrypteduserPW
                every { nanoIdGenerator.createNanoId() } returns uid
                every { userRepository.save(any()) } returns expectedUser

                userService.saveVerifiedUserInfo(dto)

                then("DB에 회원 정보를 추가해 저장할 수 있다.") {
                    verify { userRepository.save(expectedUser) }
                }
            }

        }

 

After

"이메일 인증이 완료된 경우 DB에 회원 정보를 저장할 수 있다."{
	val email = "email@email.com"	
	val userPW = "abc1234!"
	val userPWConfirm = "abc1234!"
	val nickname = "minturtle"
		
	val dto = UserRegisterDto(
	  email,
	  userPW,
	  userPWConfirm,
	  nickname
)

	val encrypteduserPW = "asldll321lslafas231412@3@!Ffa"
	val uid = "123asf"
	
	every { emailVerifier.checkVerified(email) } returns Unit	
	every { userPWEncoder.encode(userPW) } returns encrypteduserPW
	every { nanoIdGenerator.createNanoId() } returns uid
	every { userRepository.save(any()) } returns expectedUser
	
	
	userService.saveVerifiedUserInfo(dto)
	
	verify { userRepository.save(User(uid, email, encrypteduserPW, nickname)) }

}
  • 다른 사람들이 보기에는 어떨지 모르겠지만, 내눈에는 확실히 Mocking하는 부분과 변수 선언 부분을 묶어 한 곳에 놔둘수 있고, Code Depth도 깊어지지 않아서 좋은 것 같습니다.
  • 또 맨 윗줄의 문자열만 보고도 어떠한 기능에 대한 테스트인지 딱 보이기 때문에 확실히 보기 편한 것 같습니다.

마무리

  • 난 그동안 BDD가 TDD의 개선버젼 정도로 생각하고, 모든 테스트를 BDD로만 작성해 왔습니다.
  • 하지만 멘토님의 피드백 덕에 Test 코드에 대한 개념을 다시 정리할 수 있었고, 더 좋은 테스트 코드를 작성하는 것과 BDD와 TDD는 상호보완적인 개념이란 것을 알게 되었습니다.
  • 관련 Issue: https://github.com/f-lab-edu/min-ticketing/issues/8
반응형