Unscramble App : unit test
위의 페이지를 참고하여 Unscramble 앱의 단위 테스트를 진행해보자
개요
예전에 Tip Time App을 작성할 때
테스트를 찍먹해봤으니 조금은 익숙할 것이라고 생각한다
테스트는 크게 다음 3가지로 나뉜다
사용자가 적당한 행동만 했을 때를 보는 행복 경로(happy path)
사용자가 되도 않는 입력을 헀을 때를 보는 오류 경로(error path)
가능한 최대나 최소값일 때를 보는 경계 경로(boundary path)
단위 테스트란 하나의 메소드 같은 작은 요소를 독립적으로 검사하는 것이다
좋은 단위테스트에는 4가지 요소가 있다
집중 : 작은 요소로 테스트 한다
이해가능 : 코드가 가독성이 좋아야 한다
확정성 : 코드에 수정이 없는데 어떨 땐 테스트를 통과하고 어떨 땐 테스트에 실패하면 안된다
독립형 : 사람이 따로 설정해주지 않아도 알아서 진행되어야 한다
자 그럼 이제 본격적으로 테스트를 할 준비를 해보자
우선 dependencies에 JUnit을 추가해야 한다
JUnit은 Java에서 테스트를 할 때 쓰는 프레임워크이다
build.gradle (Module: Unscramble.app) 파일에 들어가서
맨 밑의 dependencies에 testImplementation 'junit:junit:4.13.2'를 추가해주고
위에 Sync Now를 클릭해준다
테스트 메소드들을 가지는 GameViewModelTest 클래스가 있는 동명의 .kt 파일을 만들어주자
Test용 패키지에 만들어야 한다
테스트 진행
GameViewModelTest.kt의 GameViewModelTest 클래스안에 테스트 관련 코드들을 작성할 것이다
우선 테스트시 자주 사용하게 될 GameViewModel 인스턴스를 하나 만들어놓자
private val viewModel = GameViewModel()
총 4가지의 경우에 대해 테스트를 진행할 것이고 상황은 다음과 같다
1. happy path - 사용자가 정확한 답을 입력하고 맞췄을 때
2. error path - 사용자가 잘못된 답을 입력했을 때
3. boundary path - 게임이 시작했을 때
4. boundary path - 게임이 끝났을 때
참고로 테스트 패키지의 WordsData.kt를 보면 getUnscrambleWord라는 함수를 볼 수 있을 것이다
이름 그대로 섞인 단어를 넣어주면 안섞인 원래의 단어가 나오는 함수이다
테스트 하는 내내 요긴하게 쓸테니 확인하고 넘어가자
1. happy path - 사용자가 정확한 답을 입력하고 맞췄을 때
테스트를 진행하는 메소드의 이름의 규칙을 다음과 같이 정하도록 하자
"테스트할 대상(오늘은 항상 gameViewModel)_주어진상황_예상되는결과"
현재와 같은 상황에서는
gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset과 같이 이름 지을 수 있다
이 메소드를 구현하는 방식을 쉽게 설명하면 다음과 같다
1. 현재 상태에서 getUnscrambledWord를 이용해 옳은 답을 알아낸다
2. 옳은 답을 updateUserGuess를 통해 입력하고 checkUserGuess로 정답인지 확인한다
3. 상태값(isGuessWordWrong, score)이 예상한대로 바뀌었는지 확인한다
코드로 바꾸면 다음과 같다
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrumbledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
assertFalse(currentGameUiState.isGuessedWordWrong)
assertEquals(SCORE_INCREASE, currentGameUiState.score)
}
컴포저블 메소드나 프리뷰 메소드처럼 앞에 @Test라는 annotation을 달아주어야한다
코드의 내용은 위에서 설명한 구현 방식의 번호 별로 구분해서보면 쉽게 이해할 수 있을 것이다
예전에 테스트를 찍먹했었지만 기억이 나지 않을 수 있으니 assert에 대해서 짚고 넘어가자면
assertion의 약자이면서 '정말 이런지 확인해보자'라고 생각하면 된다
assertFalse는 정말 거짓인지 확인해보자
asserEquals는 정말 같은지 확인해보자 같은 식이다
SCORE_INCREASE가 코드의 이해에 도움이 되지않는 이름이기 때문에 다음과 같이 상수를 선언해주자
참고로 일반 클래스 멤버 변수로 const val과 같은 형태의 상수를 선언하려고 하면
오류가 뜨면서 object나 companion 안에서 선언하라고 나온다
클래스의 멤버변수는 클래스가 인스턴스화 되면서 생성되고
object, companion object는 클래스를 로딩하는 것만으로도 생성되기 때문인 것 같다
상수는 누구보다 빠르게 생성되어있어야 하니까
어쨌든 다음과 같은 코드를 활용해서 SCORE_INCREASE의 이름을 더 직관적으로 바꿔주자
companion object {
private const val SCORE_AFTER_FIRST_CORRECT_ANSWER = SCORE_INCREASE
}
이제는 테스트를 진행하면 되는데 왼쪽에 있는 화살표를 클릭해주자
그리고 Run 어쩌구를 클릭해주자
아마도 통과됐다는 기호가 나타날 것이다
2. error path - 사용자가 잘못된 답을 입력했을 때
이제 다음 테스트 메소드들은 쉽게 쉽게 만들 수 있을 것이다
이름을 gameViewModel_IncorrectGuess_ErrorFlagSet이라고 짓고
1. 잘못된 답을 updateUserGuess로 입력하고 checkUserGuess로 정답인지 확인한다
2. 상태값(isUserGuessedWordWrong, score)이 예상대로 바뀌었는지 확인한다
의 내용을 가지도록 다음과 같이 코드를 작성한다
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
val currentGameUiState = viewModel.uiState.value
assertEquals(0, currentGameUiState.score)
assertTrue(currentGameUiState.isGuessedWordWrong)
}
아까 처럼 실행시켜보자
3. boundary path - 게임이 시작했을 때
이름은 gameViewModel_Initialization_FirstWordLoaded로 짓고
1. getUnscrambledWord를 통해 올바른 답을 구한다
2. 올바른 답을 활용해 상태값들(score, currentWordCount, isGuessedWordWrong, isGameOver)이 예상과 같은지 확인한다
의 내용을 가지도록 다음과 같이 코드를 작성한다
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrumbledWord)
assertNotEquals(unScrambledWord, gameUiState.currentScrumbledWord)
assertEquals(1, gameUiState.currentWordCount)
assertEquals(0, gameUiState.score)
assertFalse(gameUiState.isGuessedWordWrong)
assertFalse(gameUiState.isGameOver)
}
테스트를 돌려보면 실패가 뜰 것이다
currentWordCount가 1이 아니라 0이 뜨는 것이 문제인데
생각해보면 이 변수는 답을 제출해서 맞췄거나 스킵을 했을 때
1씩 커졌었기 때문에 처음부터 1일 수가 없다
그러니까 처음부터 1의 값을 가지도록 해주자
이런게 이제 테스트의 장점인 것 같다 생각 못한 오류를 발견하는 것
4. boundary path - 게임이 끝났을 때
이름은 gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly로 정하고
1. 게임이 끝날 때까지 getUnscrambledWord, updateUserGuess, checkUserGuess를 이용해
답을 맞추는 과정을 진행하면서
2. 각 스테이지가 잘 끝났는 지 상태값(score)을 통해 확인하고
3. 마지막에 잘 끝났는 지 상태값(isGameOver, currentWordCount)을 통해 확인한다
코드는 다음과 같고 쉽게 이해할 수 있을 것이다
실제로 repeat를 써보는 것은 처음인 것 같으니 잘 살펴보자
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorerectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrumbledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrumbledWord)
assertEquals(expectedScore, currentGameUiState.score)
}
assertEquals(MAX_NO_OF_WORDS, currentGameUiState.currentWordCount)
assertTrue(currentGameUiState.isGameOver)
}
'기타 > 안드로이드' 카테고리의 다른 글
36. Cupcake App : Navigation (0) | 2022.12.17 |
---|---|
35. Dessert Clicker App : ViewModel 추가 (0) | 2022.12.06 |
33. Unscramble App : App architecture와 ViewModel (2) | 2022.11.20 |
32. Dessert Clicker App : Activity Life Cycle (2) | 2022.11.15 |
31. 30 Days Of Habit App (0) | 2022.11.10 |