테스트 코드가 중요하다고 하지만 어떻게 어디서부터 시작해야할지 막막한 경우가 많습니다 (제가 그랬어요)
이런 생각으로 테스트 코드 작성을 미루고 또 미뤘습니다
솔직히 아직까진 테스트 코드에 대한 환상적인 경험을 하진 못하기도 했고 필요성도 못느꼈습니다
하지만.. 진행중인 사이드 프로젝트에서 우연찮게 공기계로 앱을 테스트하던 중 간단하지만 치명적인 오류가 다수 발견되었습니다.
만약 실제 상용 서비스에 이대로 런칭되었다면.. 상상도 하기 싫을 것 같습니다
게다가 모바일은 웹과 다르게 실시간으로 push 할 수 없기 때문에 더욱 치명적인 상황이리고 할 수 있습니다
BDD(행동주도개발)은 본글에서 다루면 내용이 너무 길어질 것 같아
외부 블로그나 자료들이 넘쳐나기 때문에 조금만 찾아보시면 될 것 같습니다!
간단하게만 BDD에 대해 간략히 말씀드리면,
출처 - https://katalon.com/resources-center/blog/bdd-testing
이라고 할 수 있습니다
TDD와 다르게 기능보다는 사용자 중심으로 서비스 시나리오 동작을 중점으로 확인합니다
테스트 코드 작성은
Given(주어진 환경) - When(행위) - Then(기대 결과)
구조에 맞춰서 작성해보겠습니다
테스트 코드를 작성하기 위해 우선 MockK에 대해 알아야 합니다
MockK은 코틀린에 최적화된 모킹 라이브러리입니다
테스트 코드를 작성하려면 테스트에 필요한 객체가 필요한데, 이 객체를 모킹하고 스텁을 설정해 코드의 단위 테스트를 쉽게 작성할 수 있도록 지원해줍니다
더 자세한 내용은 공식문서나 구글링하면 자세히 보실 수 있습니다!
먼저 시나리오를 구성하기위해 Given-When-Then구조로 생각해보겠습니다.
// 로그인 상태를 표현하는 ENUM 클래스
enum class LoginState {
INIT, EXIST, ERROR, LOADING, BLANK
}
그전에 로그인 처리를 위한 상태 Enum클래스를 참고해주시면 됩니다!
사용자는 서비스의 기존유저이며 userNum(유저번호)을 가지고 있고 기존유저를 체크하는 API가 정상적으로 작동한다고 가정하겠습니다
사용자는 기존유저인지 체크하는 행위를 실시합니다
기존유저이기 때문에 로그인 상태가 EXIST(로그인 상태 중 기존유저인 상태)가 되어야 합니다
class LoginViewModelTest {
(..)
private val testDispatcher = StandardTestDispatcher()
private lateinit var checkMemberUseCase: CheckMemberUseCase
(..)
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
(..)
checkMemberUseCase = mockk()
viewModel = LoginViewModel(
(..), checkMemberUseCase
)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `isMember에서 기존유저면 LoginState를 EXIST로 변경`() = runTest {
//Given
val userNum = "123"
val isMemberResult = true
coEvery { checkMemberUseCase(userNum) } returns Result.success(isMemberResult)
// When
viewModel.isMember(userNum)
advanceUntilIdle()
//Then
coVerify { checkMemberUseCase(userNum) }
assert(viewModel.viewState.value.loginState == LoginState.EXIST)
}
}
먼저 주요코드를 보겠습니다
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
(..)
checkMemberUseCase = mockk()
viewModel = LoginViewModel(
(..), checkMemberUseCase
)
}
기존유저인지 판단하는 UseCase와 로그인 서비스를 정의한 LoginViewModel을 준비합니다
여기서 MockK가 자동으로 객체를 모킹하고 스텁해줍니다!
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `isMember에서 기존유저면 LoginState를 EXIST로 변경`() = runTest {
//Given
val userNum = "123"
val isMemberResult = true
coEvery { checkMemberUseCase(userNum) } returns Result.success(isMemberResult)
// When
viewModel.isMember(userNum)
advanceUntilIdle()
//Then
coVerify { checkMemberUseCase(userNum) }
assert(viewModel.viewState.value.loginState == LoginState.EXIST)
}
위에서 시나리오를 구성했듯이 Given-When-Then 구조를 통해 작성합니다
Given - coEvery를 활용해 환경을 구상하고
When - LoginViewModel에 구현되어있는 기존유저인지 체크하는 함수를 실행하고
Then - 의도되로 시나리오가 성공적으로 수행했는지 검증합니다
중간에 advanceUnitlIdle()는 LoginState가 변경될 때 까지 기다리는 코드를 의미합니다
먼저 테스트 코드를 실행해볼까요?
당연히 아직 기능 코드를 작성하기 전이기 때문에 테스트 코드는 오류가 발생합니다
이제 코드를 작성해볼까요?
fun isMember(userNum: String){
viewModelScope.launch {
checkMemberUseCase(userNum)
.onFailure { setState { copy(loginState = LoginState.ERROR) } }
.onSuccess { isExist ->
when(isExist) {
true -> setState { copy(loginState = LoginState.EXIST) }
false -> setState { copy(loginState = LoginState.INIT) }
else -> setState { copy(loginState = LoginState.ERROR) }
}
}
}
}
그렇게 어려운 코드라 생각은 안하기때문에
코드에 대한 자세한 설명은 생략하겠습니다!
이제 다시 테스트 코드를 실행해볼까요?
성공적으로 되었다는 것을 알 수 있습니다!
라고 생각이 들 수도 있습니다
하지만 로그인 같은 경우 일일히 유저번호를 다르게 설정해 기존유저와 처음유저를 설정하고 심지어 오류상황도 유도하는 테스트를 직접하는 작업은 정말 번거롭습니다
이럴때 테스트 코드를 작성하면 시간적으로 엄청난 절약이 가능합니다!
그리고 본글에선 테스트를 하나만 작성했지만
로그인같은경우 기존유저, 신규유저, 로그인에러 등등 수많은 시나리오가 존재합니다.
그런경우 테스트코드들을 작성한 후
한번에 테스트 클래스를 실행할 수 있어 해당 클래스가 정상적으로 동작하는지 확인할 수 있습니다!
테스트 코드를 어떻게 시작하고 해야할지 막막한분에게 도움이 되었으면 좋겠습니다😄
처음엔 귀찮지만 점점 작성하다보면 감도잡히고 신뢰도가 높은 개발자가 되지 않을까 싶습니다!