테스트 코드 작성?!

수호·2025년 11월 11일

1. TTD(Test-Driven Development) 란?

→ TDD는 켄트 벡(Kent Back) 이 고안한 소프트웨어 개발 방법론으로, “테스트 주도 개발” 이라는 이름 그대로 테스트를 먼저 작성하고 이를 기반으로 코드를 작성하는 방식이다. TDD는 다음과 같은 반복적인 과정을 통해 이루어진다.

  • Red: 실패하는 테스트 작성
    • 먼저 작성한 테스트 케이스가 실패하도록 설계한다. 이는 구현되지 않은 기능을 명확히 정의하는 단계이다.
  • Green: 테스트를 통과하는 최소한의 코드 작성
    • 테스트가 성공하도록 간단하게 코드를 구현한다. 이 단계에서는 최소한의 구현에 집중한다.
  • Refactor: 코드 개선
    • 테스트가 통과한 후, 코드를 리팩터링하여 가동성과 유지보수성을 개선한다. 이때 테스트가 다시 실패하지 않도록 주의한다.

2. TDD의 기대 효과

  • 버그 감소: 테세트를 기반으로 코드를 작성하기 때문에 코드의 안정성이 높아진다.
  • 리팩터링 안정성: 코드 변경 시 테스트가 올바르게 동작하는지 보장
  • 명확한 설계: 테스트 작성 과정에서 인터페이스와 로직이 자연스럽게 설계된다.

3. 어디까지 테스트를 진행할 것인지?

  • 아직 정해지진 않았지만 우리팀은 비즈니스 로직까지만 테스트 코드를 진행하면 좋을 것 같다.

4. 테스트 코트 작성 방식

core:data:source / LocalUserDataSourceTest

package com.bw.data.local.source

import com.bw.data.local.dao.FakeUserDao
import com.bw.core.data.local.db.UserDatabase
import com.bw.core.data.local.entity.UserEntity
import com.bw.core.data.local.source.LocalUserDataSource
import junit.framework.TestCase
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals

class LocalUserDataSourceTest {

    private lateinit var dataSource: LocalUserDataSource

    @Before
    fun setup(){
        // Room DB 대신 Fake DAO 사용
        val fakeDao = FakeUserDao()
        dataSource = LocalUserDataSource(fakeDao)
    }

    @Test
    fun insert_and_get_user() = runTest {
        val user = UserEntity(user_phone = "01012345678", user_name = "홍길동")
        dataSource.insertUser(user)

        val count = dataSource.getUserByPhoneAndName("01012345678", "홍길동").first()
        TestCase.assertEquals(1, count)

        val users = dataSource.getAllUsers()

        // ✅ 성공 로그 출력
        println("✅ LocalUserDataSourceTest success → user not found, count: $count / users : ${users.toList()}")

    }

}

core:data:source / UserRepositoryImplTest

package com.bw.data.local.repository

import com.bw.core.data.repository.UserRepositoryImpl
import com.bw.core.domain.model.User
import com.bw.data.local.source.FakeLocalUserDataSource
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import org.junit.Test

class UserRepositoryImplTest {

    private val repository = UserRepositoryImpl(FakeLocalUserDataSource())

    @Test
    fun insert_and_fetch_user_success() = runTest {
        val user = User(phone = "01012345678", name = "홍길동")
        repository.insertUser(user)

        val count = repository.getUserByPhoneAndName("01012345678", "홍길동").first()
        assertEquals(1, count)

        val users = repository.getAllUsers()

        // ✅ 성공 로그 출력
        println("✅ UserRepositoryImplTest success → user not found, count: $count / users : ${users.toList()}")
    }
}

core:domain:usecase / CheckUserUseCaseTest

package com.bw.domain.usecase

import com.bw.core.domain.model.User
import com.bw.core.domain.usecase.CheckUserUseCase
import com.bw.core.testing.repository.FakeUserRepository
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test

class CheckUserUseCaseTest {

    private lateinit var fakeUserRepository: FakeUserRepository
    private lateinit var checkUserUseCase: CheckUserUseCase

    @Before
    fun setup(){
        fakeUserRepository = FakeUserRepository()
        checkUserUseCase = CheckUserUseCase(fakeUserRepository)
    }

    // ✅ 성공 로그 출력
    @Test
    fun `when user exists then return count 1`() = runTest { // User가 존재할 경우 -> 1을 반환
        // given (준비)
        val user = User(phone = "01012345678", name = "홍길동")
        fakeUserRepository.insertUser(user)

        // when (실행)
        val count = checkUserUseCase("01012345678", "홍길동").first()

        // then (검증)
        assertEquals(1, count)
        println("✅ CheckUserUseCaseTest success → user count: $count")
    }

    @Test
    fun `when user does not exist then return count 0`() = runTest { // User가 존재하지 않을 경우 -> 0을 반환
        // given
        fakeUserRepository.insertUser(User(phone = "01099999999", name = "이순신"))

        // when
        val count = checkUserUseCase("01012345678", "홍길동").first()

        // then
        assertEquals(0, count)
        println("✅ CheckUserUseCaseTest success → user not found, count: $count")
    }

}

feature:login:ui / LoginViewModelTest

package com.bw.feature.login.ui

import app.cash.turbine.test
import com.bw.core.domain.model.User
import com.bw.core.domain.usecase.CheckUserUseCase
import com.bw.core.domain.usecase.InsertUserUseCase
import com.bw.core.testing.repository.FakeUserRepository
import com.bw.core.ui.event.UiEvent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals

@OptIn(ExperimentalCoroutinesApi::class)
class LoginViewModelTest {

    private lateinit var viewModel: LoginViewModel
    private lateinit var fakeRepository: FakeUserRepository

    @Before
    fun setup() {

        fakeRepository = FakeUserRepository()

        val fakeCheckUserUseCase = CheckUserUseCase(fakeRepository)
        val fakeInsertUserUseCase = InsertUserUseCase(fakeRepository)

        viewModel = LoginViewModel(fakeInsertUserUseCase,fakeCheckUserUseCase)
    }

    @Test
    fun `전화번호 미입력시 토스트 이벤트 발생`() = runTest {
        viewModel.updatePhone("")
        viewModel.updateName("홍길동")

        viewModel.uiEvent.test {
            viewModel.signIn()
            val event = awaitItem() as UiEvent.ShowToast
            assertEquals("전화번호를 입력해주세요.", event.message)
            println("💡 event.message = $event")
            println("✅ 전화번호 미입력 테스트 성공")
        }
    }

    @Test
    fun `전화번호 자릿수 오류시 토스트 이벤트 발생`() = runTest {
        viewModel.updatePhone("0101234")
        viewModel.updateName("홍길동")

        viewModel.uiEvent.test {
            viewModel.signIn()
            val event = awaitItem() as UiEvent.ShowToast
            assertEquals("휴대폰 자릿수를 확인해주세요.", event.message)
            println("💡 event.message = $event")
            println("✅ 전화번호 자릿수 오류 테스트 성공")
        }
    }

    @Test
    fun `이름 미입력시 토스트 이벤트 발생`() = runTest {
        viewModel.updatePhone("01012345678")
        viewModel.updateName("")

        viewModel.uiEvent.test {
            viewModel.signIn()
            val event = awaitItem() as UiEvent.ShowToast
            assertEquals("이름을 입력해주세요.", event.message)
            println("💡 event.message = $event")
            println("✅ 이름 미입력 테스트 성공")
        }
    }

    @Test
    fun `존재하지 않는 유저면 다이얼로그 이벤트 발생`() = runTest {
        viewModel.updatePhone("01000000000")
        viewModel.updateName("없는사람")

        viewModel.uiEvent.test {
            viewModel.signIn()
            val event = awaitItem() as UiEvent.ShowDialog
            assertEquals("로그인 실패", event.title)
            println("💡 event.message = $event")
            println("✅ 존재하지 않는 유저 테스트 성공: ${event.message}")
        }
    }

    @Test
    fun `존재하는 유저면 Navigate 이벤트 발생`() = runTest {
        fakeRepository.insertUser(User(0,"01012345678", "홍길동"))
        viewModel.updatePhone("01012345678")
        viewModel.updateName("홍길동")

        viewModel.uiEvent.test {
            viewModel.signIn()
            val event = awaitItem()
            assert(event is UiEvent.Navigate)
            println("💡 event.message = $event")
            println("✅ 로그인 성공 테스트 성공 (Navigate 이벤트 발생)")
        }
    }

}
profile
처음부터 다시 시작!!

0개의 댓글