이 포스팅은 unscramble word game 레포지토리에 있는 Viewmodel과 Test 코드를 바탕으로 작성된 설명이다.
순서가 바뀐 단어를 보고 어떤 단어인지 사용자가 맞추는 퀴즈 기능의 게임이다.상태관리와 로직을 다루는 GameViewmodel의 메서드를 단위테스트하기 위해 시나리오를 설정하고, 이에 맞는 테스트하는 작성 과정을 설명해보았다.
성공 경로, 실패 경로, 경계 사례 1가지씩 정리해보았다.
GameViewmodel은 해당 테스트와 시나리오를 이해할 수 있을 수 정도의 메서드만 표시해보았다.
package com.example.unscramble.data
const val MAX_NO_OF_WORDS = 10
const val SCORE_INCREASE = 20
// List with all the words for the Game
val allWords: Set<String> =
setOf(
"at",
"sea",
"home",
"arise",
"banana",
"android",
"birthday",
"briefcase",
"motorcycle",
"cauliflower"
)
/**
* Maps words to their lengths. Each word in allWords has a unique length. This is required since
* the words are randomly picked inside GameViewModel and the selection is unpredictable.
*/
private val wordLengthMap: Map<Int, String> = allWords.associateBy({ it.length }, { it })
internal fun getUnscrambledWord(scrambledWord: String) = wordLengthMap[scrambledWord.length] ?: ""
class GameViewModel : ViewModel() {
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()
var userGuess by mutableStateOf("")
private set
private var usedWords: MutableSet<String> = mutableSetOf()
private lateinit var currentWord: String
init {
resetGame()
}
fun resetGame() {
usedWords.clear()
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
fun updateUserGuess(guessedWord: String){
userGuess = guessedWord
}
fun checkUserGuess() {
if (userGuess.equals(currentWord, ignoreCase = true)) {
val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
updateGameState(updatedScore)
} else {
_uiState.update { currentState ->
currentState.copy(isGuessedWordWrong = true)
}
}
updateUserGuess("")
}
...중략...
}
data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 1,
val score: Int = 0,
val isGuessedWordWrong: Boolean = false,
val isGameOver: Boolean = false
)
1.시나리오
유저가 제시된 퀴즈를 보고 단어를 입력하면, 그 단어가 정답인 것을 확인하고 점수를 부여해야 한다.
2.뷰모델 시나리오
updateUserGuess() 메서드가 올바른 추측를 인수로 받아서 단어로 호출되어야 함
-> 올바른 단어인지 상태변수 isGuessedWordWrong 값 확인 필요
checkUserGuess() 메서드가 호출된 후 올바른 점수변경이 되어야 함
-> 점수변경이 되었는지 상태변수 score 값 확인 필요
3.테스트 코드
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
//checkUserGuess()가 isGuessedWordWrong 값을 false로 업데이트시켰는지 확인
assertFalse(currentGameUiState.isGuessedWordWrong)
//score가 기대한 점수값인지 확인
assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
}
}
1.시나리오
유저가 제시된 퀴즈를 보고 단어 'and'를 입력했지만, 그 단어가 정답이 아니었기 때문에 에러를 표시해야 한다.
2.뷰모델 시나리오
updateUserGuess() 메서드가 단어'and'를 인수로 받아서 호출되어야 함
-> 올바른 단어인지 상태변수 isGuessedWordWrong 값 확인 필요
checkUserGuess() 메서드가 호출된 후 올바른 점수변경이 되어야 함
-> 점수변경이 되었는지 상태변수 score 값 확인 필요
3.테스트 코드
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// 인풋 제공
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
val currentGameUiState = viewModel.uiState.value
//score값이 그대로 0점인지 확인
assertEquals(0, currentGameUiState.score)
// checkUserGuess()메서드가 isGuessedWordWrong을 업데이트했는지 확인
assertTrue(currentGameUiState.isGuessedWordWrong)
}
}
1.시나리오
첫 번째 경계 사례는 초기화 상태이다. 간단히 초기화가 예상대로 되었는지 상태변수들 값을 어셜션하자.
2.뷰모델 시나리오
gameUiState의 값 확인하기 (currentScrambledWord, currentWordCount, score, isGuessedWordWrong, isGameOver)
3.테스트 코드
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
assertTrue(gameUiState.currentWordCount == 1)
assertTrue(gameUiState.score == 0)
assertFalse(gameUiState.isGuessedWordWrong)
assertFalse(gameUiState.isGameOver)
}
}
1.시나리오
두 번째 경계 사례는 사용자가 모든 단어를 추측한 후 UI 상태를 테스트하는 것이다.
게임이 진행되면서 플레이어가 올바르게 단어를 맞춘다고 가정한다. MAX_NP_OF_WORD 상수값은 20이다. 20번만큼 반복 후의 점수상태, 단어 수 상태, 게임 종료 여부 상태를 확인할 것이다.
2.뷰모델 시나리오
각 반복에서 플레이어의 추측을 업데이트하고, 추측이 맞는지 확인한 다음, UI 상태(uiState)를 업데이트한다.
반복적으로 MAX_NO_OF_WORDS만큼 단어를 맞추며, 각 정답 이후 점수가 SCORE_INCREASE만큼 증가하는지 확인한다.
단어 수가 최신 상태인지 확인, 게임이 끝났는지 확인한다.
3.테스트 코드
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
// 각 정답 이후에 점수가 올바르게 업데이트되었는지 확인한다.
assertEquals(expectedScore, currentGameUiState.score)
}
// 모든 질문에 답변한 후 현재 단어 수가 최신 상태인지 확인한다.
assertEquals(MAX_NO_OF_WORDS, currentGameUiState.currentWordCount)
// 10개의 질문에 답변한 후 게임이 끝났는지 확인한다.
assertTrue(currentGameUiState.isGameOver)
}
}