Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

gdsc-konkuk/23-24-study-1st-android-yonghaJu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

85 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ› ๏ธ ViewModel ํ…Œ์ŠคํŠธ ์ž๋™ํ™” + @Before @After ์–ด๋…ธํ…Œ์ด์…˜ ์ œ๊ฑฐ ๋ฆฌํŽ™ํ† ๋ง ๊ณผ์ • ๐Ÿ’ก

์„œ๋ก 

GDSC Konkuk ์•ˆ๋“œ๋กœ์ด๋“œ ์Šคํ„ฐ๋”” 5์ฃผ์ฐจ ๊ณผ์ œ๋Š” ์ž์œจ ๊ณผ์ œ์ด๋‹ค.

๊ฐœ์ธ๋งˆ๋‹ค ํ•˜๊ณ  ์‹ถ์—ˆ๋˜ ๊ฒƒ, ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ, ๋ฆฌํŽ™ํ† ๋ง ํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ๋“ค์— ๋Œ€ํ•ด ์ž์œ ๋กญ๊ฒŒ ์ ์šฉํ•ด๋ณด๊ณ  ์ •๋ฆฌํ•˜๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋‚˜๋Š” 5์ฃผ์ฐจ ๊ณผ์ œ๋กœ ๋ทฐ๋ชจ๋ธ์— Unit ํ…Œ์ŠคํŠธ ์ ์šฉ ๋ฐ GitAction์„ ํ†ตํ•œ ์ž๋™ ํ…Œ์ŠคํŠธ๋ฅผ ๊ณผ์ œ๋กœ ์ง„ํ–‰ํ•˜๊ธฐ๋„ ํ–ˆ๋‹ค.

ํ•ด๋‹น ๊ณผ์ •์—์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์— ์ง์ ‘์ ์ธ ์—ฐ๊ด€์ด ์—†๋Š” ๋กœ์ง๊ณผ ๊ฐ์ฒด ๊ด€๋ฆฌ์— ๋Œ€ํ•œ ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•ด๋ณด์•˜๊ณ  ๋‚ด๊ฐ€ ์„ ํƒํ•œ ๋ฐฉ๋ฒ•์„ ์ถ”๊ฐ€์ ์œผ๋กœ ์ ์–ด๋ณด์•˜๋‹ค.

์ œ๊ฐ€ ์„ ํƒํ•œ ๋ฐฉ๋ฒ•์€ ์ •๋‹ต์ด ์•„๋‹ˆ๋ฉฐ ์–ธ์ œ๋“  ํ”ผ๋“œ๋ฐฑ ์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.



Unit Test ์ž‘์„ฑ

์˜์กด์„ฑ ์ถ”๊ฐ€

viewModel ์•ˆ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ง์€ ๋Œ€๋ถ€๋ถ„ Flow๋ฅผ ์‚ฌ์šฉํ•ด ์ƒํƒœ๋ฅผ ๋ณด๊ด€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด kotlinx-coroutines-test ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•œ๋‹ค. Kotest ๋“ฑ ์ฝ”ํ‹€๋ฆฐ์œผ๋กœ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ์žˆ์ง€๋งŒ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ์‹œ ๊ธฐ๋ณธ์œผ๋กœ ์ถ”๊ฐ€๋˜์–ด์žˆ๋Š” Junit์„ ์‚ฌ์šฉํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด์ž.

์ถ”๊ฐ€์ ์œผ๋กœ collect ์ฝ”๋ฃจํ‹ด์„ ๋งŒ๋“œ๋Š” ํŽธ๋ฆฌํ•œ API์™€ Flow๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ธฐํƒ€ ํŽธ์˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์„œ๋“œํŒŒํ‹ฐ Turbine ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ์ถ”๊ฐ€ํ•ด์ฃผ์ž.

    testImplementation("junit:junit:$junitVerison")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineTestVersion")
    testImplementation("app.cash.turbine:turbine:$turbineVersion")


๋”๋ธ” ๋งŒ๋“ค๊ธฐ

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ ๊ฐ์ฒด์— ์˜์กดํ•˜๋ฉด ์•ˆ๋œ๋‹ค ๋”ฐ๋ผ์„œ ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ๋Œ€์‹ ํ•  ์Šคํ„ดํŠธ๋งจ ๊ฐ™์€ ์กด์žฌ๊ฐ€ ํ•„์š”ํ•œ๋ฐ ์ด๋ฅผ ๋”๋ธ”์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

๋”ฐ๋ผ์„œ ๋ทฐ๋ชจ๋ธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋“ค์— ๋Œ€ํ•œ Fake ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•˜๋Š”๋ฐ ๋Œ€๋ถ€๋ถ„ Flow๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋–ป๊ฒŒ Fake ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์ง€ ๊ณ ๋ฏผํ–ˆ๊ณ  ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์‚ดํŽด ๋ณด์•˜๋‹ค.

๊ณต์‹ ๋ฌธ์„œ ๋‚ด์šฉ ์ค‘ ํ…Œ์ŠคํŠธ ์ค‘ Flow ์ˆ˜์ง‘ํ•˜๊ธฐ ํŒŒํŠธ์—์„œ ๋‹ค๋ฃจ๋Š” ์˜ˆ์ œ ์ฝ”๋“œ๋ฅผ ๋ณด๊ณ  ๋‹ต์„ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

StateFlow ์™€ SharedFlow ๋ชจ๋‘ Flow ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์†ํ•˜๊ธฐ ๋•Œ๋ฌธ์— Fake ๊ฐ์ฒด ๋‚ด๋ถ€์—์„œ Shared, State Flow๋ฅผ ์‚ฌ์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๊ด€ํ•˜๊ณ  Flow๋กœ ํƒ€์ž… ์บ์ŠคํŒ… ๋ฐ˜ํ™˜ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

class FakeTodoRepository : TodoRepository {
    
    // ๋‚ด๋ถ€์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๊ด€
    private val todos = MutableStateFlow(listOf<TodoItem>())
    
    // ๋ฐ˜ํ™˜ํ•  ๋•Œ Flow๋กœ ํƒ€์ž…์œผ๋กœ ๋ฐ˜ํ™˜
    override fun getTodos(): Flow<List<TodoItem>> = todos

    override suspend fun setTodo(todoItem: TodoItem) {
        todos.emit(todos.value.map { item -> if (item.id == todoItem.id) todoItem else item })
    }

    // ...
}

๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ์ชฝ์—์„œ๋Š” ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ˆ˜์ง‘ํ•ด์ค˜์•ผ ํ•˜๋Š”๋ฐ ๊ณต์‹๋ฌธ์„œ์—์„œ ์ž‘์„ฑ๋œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

    // Create an empty collector for the StateFlow
    backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
        viewModel.score.collect()
    }

์ฃผ์˜: ์ด๋Ÿฌํ•œ ์˜ต์…˜์œผ๋กœ ๋งŒ๋“  StateFlow๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋Š” ํ…Œ์ŠคํŠธ ์ค‘์— ์ˆ˜์ง‘๊ธฐ๊ฐ€ ํ•˜๋‚˜ ์ด์ƒ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด stateIn ์—ฐ์‚ฐ์ž๋Š” ๊ธฐ๋ณธ ํ๋ฆ„ ์ˆ˜์ง‘์„ ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ณ  StateFlow์˜ ๊ฐ’์€ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.



ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

ํ…Œ์ŠคํŠธ ๊ณผ์ •์—์„œ suspend ํ•จ์ˆ˜ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ runTest ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

์ด๋•Œ ์ปจํ…์ŠคํŠธ๋ฅผ EmptyCoroutineContext ๋กœ ๋ฐ›๋Š” ๋ถ€๋ถ„์ด ๋–„๋ฌธ์— dispatcher๋ฅผ ์–ด๋–ป๊ฒŒ ์ •ํ•ด์ค˜์•ผ ํ•  ์ง€ ๊ณ ๋ฏผ์ด์—ˆ์ง€๋งŒ ์ผ๋‹จ ๊ณต์‹๋ฌธ์„œ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉฐ ๋ทฐ๋ชจ๋ธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋งˆ์ € ์ž‘์„ฑํ•ด๋ณด์ž

public fun runTest(
    context: CoroutineContext = EmptyCoroutineContext,
    timeout: Duration = DEFAULT_TIMEOUT,
    testBody: suspend TestScope.() -> Unit
): TestResult {
    check(context[RunningInRunTest] == null) {
        "Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details."
    }
    return TestScope(context + RunningInRunTest).runTest(timeout, testBody)
}


์ž‘์„ฑํ•œ ๋ทฐ๋ชจ๋ธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฐ ์ด์Šˆ

// ...
    @Test
    @OptIn(ExperimentalCoroutinesApi::class)
    fun `๋žœ๋ค ์‚ฌ์ง„์„ ๋ˆŒ๋ €์„ ๋•Œ ๋žœ๋คํ•˜๊ฒŒ ์‚ฌ์ง„ URL์ด ๋ฐ›์•„์™€ ์ง€๋Š”์ง€`() = runTest {
        backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
            editViewModel.userPhoto.collect()
        }

        // When
        editViewModel.setRandomPhoto()

        // Then
        assertEquals(FakePhotoRepository.RANDOM_URL, editViewModel.userPhoto.value)
    }
// ...

EditViewModel ์—์„œ ๋žœ๋คํ•œ ์‚ฌ์ง„์„ ๊ฐ€์ ธ์˜ค๊ฒŒํ•˜๋Š” ๋ถ€๋ถ„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ์‹คํ–‰์‹œ์ผœ๋ณด๋‹ˆ ์•„๋ž˜์™€ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค

Exception in thread "Test worker" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used


๋ณด์•„ํ•˜๋‹ˆ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋˜๋Š” ์ฝ”๋ฃจํ‹ด ์ปจํ…์ŠคํŠธ ๋ฉ”์ธ์œผ๋กœ ์ง€์ •ํ•ด์ฃผ์ง€ ์•Š์•„์„œ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”์— ์‹คํŒจํ–ˆ๋‹ค๋Š” ๊ฒƒ์œผ๋กœ ํŒ๋‹จ๋œ๋‹ค.


์‹ค์ œ๋กœ EditViewModel ์—์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” userPhoto: StateFlow<String?> ๊ฐ์ฒด์˜ ์„ ์–ธ๋ถ€๋ถ„์„ ๋ณด๋ฉด viewModelScope ๋ฅผ ์‚ฌ์šฉํ•ด์„œ stateFlow๋ฅผ ๋งŒ๋“œ๋Š”๋ฐ ๋ทฐ๋ชจ๋ธ ์Šค์ฝ”ํ”„๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Dispatcher.Main.immediate ๋””์ŠคํŽ˜์ฒ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๊ฐ€ ์•ˆ๋˜๋Š” ๊ฒƒ์œผ๋กœ ์ดํ•ด๋œ๋‹ค.

    // EditViewModel.kt
    val userPhoto = userRepository.userPhotoUrlFlow.stateIn(
        scope = viewModelScope, // Dispatcher.Main.immediate ์‚ฌ์šฉ
        started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT),
        initialValue = null,
    )

์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ค‘ ๋งŽ์ด ์ฑ„ํƒํ•˜๊ณ  ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ์•„ ์‚ฌ์šฉํ•ด๋ณด์•˜๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด TestWatcher() ํด๋ž˜์Šค๋ฅผ ์ƒ์†ํ•œ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ ๋‹ค.

์ด ํด๋ž˜์Šค๋Š” ํ…Œ์ŠคํŠธ์— ์‹œ์ž‘๋ถ€๋ถ„๊ณผ ์ข…๋ฃŒ ์‹œ์ ์—์„œ ๊ฐ๊ฐ starting, finished ์ฝœ๋ฐฑ์ด ์‹คํ–‰๋˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด ์•ˆ์—์„œ Dispatchers.setMain(testDispatcher), Dispatchers.resetMain() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด์ค€๋‹ค.

๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์•ˆ์— MainDispatcherRule ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  @get:Rule ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ์ฃผ๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์žฌ์‹คํ–‰ํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ฒŒ ๋œ๋‹ค.

// ํ…Œ์ŠคํŠธ ๋ฃฐ
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

// ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
class EditViewModelTest {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule() 
    // ...
}


@After, @Before -> Rule ๋ฆฌํŽ™ํ† ๋ง

๋ฆฌํŽ™ํ† ๋ง์ด ํ•„์š”ํ•˜๋‹ค ์ƒ๊ฐํ•œ ์ด์œ 

  1. @Before, @After ์–ด๋…ธํ…Œ์ด์…˜ ์ฝ”๋“œ์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•˜์ง€๋งŒ ๋กœ์ง์ด ๋ถ„์‚ฐ๋จ.
  2. ํ…Œ์ŠคํŠธ์™€ ์ง์ ‘์ ์ธ ์—ฐ๊ด€์ด ์—†๋Š” ๋กœ์ง๊ณผ ๊ฐ์ฒด๋“ค์ด ๊ณต๊ฐœ๋จ.

์ด์ „์— ๋งŒ๋“ค์—ˆ๋˜ MainDispatcherRule ํด๋ž˜์Šค์—์„œ ์‚ฌ์šฉ๋œ Dispatcher.resetMain() ํ•จ์ˆ˜์˜ ๊ตฌํ˜„๋ถ€๋ฅผ ๋ณด์•˜์„ ๋•Œ ์•„๋ž˜์™€ ๊ฐ™์€ ์ฃผ์„์ด ์žˆ์—ˆ๋‹ค.

(...) and so should be used in tear down (@After) methods.

์ฆ‰, TestWatcher ํด๋ž˜์Šค๋Š” ํ…Œ์ŠคํŠธ ์ „ํ›„ ๋™์ž‘์„ ์„ค์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๊ณ , ์ด๋Š” ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์•ˆ์—์„œ ์‚ฌ์šฉํ–ˆ๋˜ @Before @After ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ๋„ ๊ฐ™์€ ์—ญํ• ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ธฐ์กด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ๋กœ๋ถ€ํ„ฐ ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— @Before ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ธ setUp() ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ด ๊ฐ์ฒด๋“ค์„ ์ดˆ๊ธฐํ™”ํ•ด์ค˜์•ผ ํ–ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ฐ์ฒด๋“ค์ด ์ ์ฐจ ๋งŽ์•„์ง„๋‹ค๋ฉด ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์•ˆ์— ์ดˆ๊ธฐํ™”, ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ ๋“ฑ ํ…Œ์ŠคํŠธ์™€ ์ง์ ‘์ ์ธ ๊ด€๋ จ์ด ์—†๋Š” ์ฝ”๋“œ๋“ค์˜ ์–‘์ด ๋งŽ์•„์งˆ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ์ฝ”๋“œ๋“ค์„ TestWatcher ํด๋ž˜์Šค์— ์—ญํ•  ์œ„์ž„ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

// ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
class EditViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private lateinit var fakeUserRepository: UserRepository
    private lateinit var fakePhotoRepository: PhotoRepository // ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉ๋˜์ง€ ์•Š์„ ๊ฐ์ฒด
    private lateinit var setRandomPhotoUseCase: SetRandomPhotoUseCase // ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉ๋˜์ง€ ์•Š์„ ๊ฐ์ฒด
    private lateinit var editViewModel: EditViewModel

    // ํ…Œ์ŠคํŠธ์— ์ง์ ์ ์ธ ์—ฐ๊ด€์ด ์—†๋Š” ์‚ฌ์ „ ๋กœ์ง
    @Before
    fun setUp() {
        fakeUserRepository = FakeUserRepository()
        fakePhotoRepository = FakePhotoRepository()
        setRandomPhotoUseCase = SetRandomPhotoUseCase(fakePhotoRepository, fakeUserRepository)
        editViewModel = EditViewModel(
            fakeUserRepository,
            setRandomPhotoUseCase,
        )
    }
        
    @Test
    fun test1() {
        // ... 
    }
}

๋ฆฌํŽ™ํ† ๋ง ํ›„ ์–ป์€ ์žฅ์ 

  • ์ฑ…์ž„ ๋ถ„๋ฆฌ: ์ง์ ‘์ ์ธ ํ…Œ์ŠคํŠธ ์ด์™ธ์˜ ์ฑ…์ž„์„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ๋ถ„๋ฆฌ

    ๊ฐ์ฒด์˜ ์ƒ์„ฑ์ด๋‚˜ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ๊ฐ™์€ ์ฝ”๋“œ๋“ค์ด ๋งŽ์•„์ ธ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๊ฐ€ ๋‚œ๋…ํ™”๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Œ

  • ์บก์Šํ™”: ์‹ค์ œ๋กœ ์‚ฌ์šฉ๋˜์ง€ ์•Š๊ณ  ์ƒ์„ฑ์—๋งŒ ํ•„์š”ํ•œ fake ๊ฐ์ฒด๋ฅผ ๊ฐ์ถค

    fakePhotoRepository, setRandomPhotoUseCase ๊ฐ™์ด ๋ทฐ๋ชจ๋ธ ์ƒ์„ฑ์‹œ์— ํ•„์š”ํ•˜์ง€๋งŒ ์ง์ ‘ ์ ‘๊ทผํ•  ํ•„์š”๊ฐ€ ์—†๋Š” ๊ฐ์ฒด๋“ค์„ private ํ•˜๊ฒŒ ๊ฐ์ถœ ์ˆ˜ ์žˆ๋‹ค.

// ์ฑ…์ž„ ๋ถ„๋ฆฌ ๋ฆฌํŽ™ํ† ๋ง ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
class EditViewModelTest {

    @get:Rule
    val editViewModelTestRule = EditViewModelTestRule()

    @Test
    fun test1() = with(editViewModelTestRule){
        // ... 
    }
}

// ํ…Œ์ŠคํŠธ ๋ฃฐ
// MainDispatcherRule์„ ์ƒ์†ํ•ด์„œ ๊ธฐ์กด ๋™์ž‘(setMain)์„ ์œ ์ง€
class EditViewModelTestRule : MainDispatcherRule() {
    
    lateinit var editViewModel: EditViewModel               // ํ…Œ์ŠคํŠธ์ฝ”๋“œ์— ๊ณต๊ฐœ ์‹œํ‚ฌ ๊ฐ์ฒด 
    lateinit var fakeUserRepository: FakeUserRepository     // ํ…Œ์ŠคํŠธ์ฝ”๋“œ์— ๊ณต๊ฐœ ์‹œํ‚ฌ ๊ฐ์ฒด 
    
    override fun starting(description: Description) {
        super.starting(description)

        val fakePhotoRepository = FakePhotoRepository()
        fakeUserRepository = FakeUserRepository()
        val setRandomPhotoUseCase = SetRandomPhotoUseCase(fakePhotoRepository, fakeUserRepository)
        editViewModel = EditViewModel(
            fakeUserRepository,
            setRandomPhotoUseCase,
        )
    }
}


ํ…Œ์ŠคํŠธ ์ž๋™ํ™”

GitAction์œผ๋กœ PR ๋‹จ์œ„๋กœ ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰

Cucumber ๋“ฑ ์„œํŠธํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด UI ํ…Œ์ŠคํŠธ ์ž๋™ํ™”์™€ BDD(Back-End Driven Development)๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง„๋‹ค. BDD๋Š” ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ UI๋ฅผ ํ˜•์‹์„ ์ œ๊ณต๋ฐ›๊ธฐ ๋•Œ๋ฌธ์— ์•ฑ์„ ์—…๋ฐ์ดํŠธ, ๋ฐฐํฌํ•˜์ง€ ์•Š์•„๋„ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ ์šฉ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋ฒˆ ์Šคํ„ฐ๋”” ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ GitAction์„ ํ†ตํ•ด PR ๋‹จ์œ„๋กœ ํ…Œ์ŠคํŠธ ์ž๋™ํ™”ํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

๋ฐฉ๋ฒ•์€ ๊ฐ„๋‹จํ•œ๋ฐ ๋ ˆํฌ์ง€ํ† ๋ฆฌ root ์•„๋ž˜์— .github/workflows/ci.yml ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ๋œ๋‹ค.

๊ฒผ์—ˆ๋˜ ์ด์Šˆ๋“ค์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

  1. jdk ๋ฒ„์ „ ํ˜ธํ™˜ x, 11 -> 17๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ–ˆ๋”๋‹ˆ ๋ฌธ์ œ ํ•ด๊ฒฐ

  2. local.properties ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ

    build.gradle ์—์„œ api access token ์„ ๋กœ์ปฌ ํ”„๋กœํผํ‹ฐ์—์„œ ๊ฐ€์ ธ์˜ค๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— git secret ์— ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค.


์ถ”๊ฐ€์ ์œผ๋กœ apk ๋ฅผ ๋งŒ๋“ค์–ด github์— ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ slack, discord ์— ๋ด‡์„ ๋งŒ๋“ค๊ณ  api๋ฅผ ์š”์ฒญํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋กœ์ปฌ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

image
name: gdsc_test_ci
on:
  pull_request:
    branches: [ "main" ]
  workflow_dispatch:
    inputs:
      tags:
        description: 'Test scenario tags'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'zulu'
          cache: gradle

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Create Local Properties
        run: echo '${{ secrets.LOCAL_PROPERTIES }}' > ./local.properties

      - name: Start gradlew test
        run: ./gradlew test

Releases

No releases published

Packages

No packages published

Languages