การทดสอบขั้นตอนของ Kotlin ใน Android

วิธีที่คุณทดสอบหน่วยหรือโมดูลที่สื่อสารกับ flow ขึ้นอยู่กับว่าวัตถุที่กำลังทดสอบใช้โฟลว์เป็นอินพุตหรือเอาต์พุต

  • หากผู้เข้าร่วมทดสอบสังเกตเห็นโฟลว์ คุณจะสร้างโฟลว์ภายใน ทรัพยากร Dependency ปลอมที่คุณควบคุมได้จากการทดสอบ
  • หากหน่วยหรือโมดูลแสดงโฟลว์ คุณจะอ่านและยืนยันได้ หรือ หลายรายการที่ปล่อยออกมาจากขั้นตอนหนึ่งในการทดสอบ

การสร้างโปรดิวเซอร์ปลอม

เมื่อผู้เข้าร่วมทดสอบเป็นผู้บริโภคของขั้นตอนใดๆ วิธีทั่วไปในการทดสอบ คือการแทนที่ผู้ผลิตด้วยการติดตั้งใช้งานปลอม ตัวอย่างเช่น ระบุ คลาสที่สังเกตที่เก็บซึ่งนำข้อมูลจากแหล่งข้อมูล 2 แหล่งมาไว้ใน เวอร์ชันที่ใช้งานจริง:

เรื่องที่อยู่ภายใต้การทดสอบและชั้นข้อมูล
รูปที่ 1 หัวข้อที่กำลังทดสอบและข้อมูล เลเยอร์

หากต้องการทดสอบเชิงกำหนด คุณอาจแทนที่ที่เก็บและที่เก็บดังกล่าว ทรัพยากร Dependency ที่มีที่เก็บปลอมซึ่งจะส่งข้อมูลปลอมแบบเดียวกันเสมอ

ทรัพยากร Dependency ถูกแทนที่ด้วยการใช้งานปลอม
รูปที่ 2 การขึ้นต่อกันจะถูกแทนที่ด้วยข้อมูลปลอม นี้

หากต้องการปล่อยชุดค่าที่กำหนดไว้ล่วงหน้าในโฟลว์ ให้ใช้เครื่องมือสร้าง flow ดังนี้

class MyFakeRepository : MyRepository {
    fun observeCount() = flow {
        emit(ITEM_1)
    }
}

ในการทดสอบ ระบบแทรกที่เก็บปลอมนี้ แทนที่ที่เก็บจริง การใช้งาน:

@Test
fun myTest() {
    // Given a class with fake dependencies:
    val sut = MyUnitUnderTest(MyFakeRepository())
    // Trigger and verify
    ...
}

เมื่อควบคุมผลลัพธ์ของสิ่งที่กำลังทดสอบได้แล้ว คุณก็ทำสิ่งต่อไปนี้ได้ ยืนยันว่าทำงานได้ถูกต้องโดยตรวจสอบเอาต์พุต

ยืนยันการปล่อยก๊าซจากการไหลเวียนในการทดสอบ

หากตัวอย่างที่อยู่ระหว่างการทดสอบแสดงขั้นตอน การทดสอบก็จะต้องยืนยัน เกี่ยวกับองค์ประกอบของสตรีมข้อมูล

สมมติว่าที่เก็บของตัวอย่างก่อนหน้านี้แสดงโฟลว์:

ที่เก็บที่มีทรัพยากร Dependency ปลอมซึ่งแสดงโฟลว์
รูปที่ 3 ที่เก็บ (หัวข้อที่อยู่ระหว่างการทดสอบ) ที่มีข้อมูลปลอม ทรัพยากร Dependency ที่แสดงโฟลว์

สำหรับบางการทดสอบ คุณจะต้องตรวจสอบเฉพาะปริมาณการปล่อยก๊าซครั้งแรกหรือระยะเวลาจำกัด จำนวนรายการที่มาจากโฟลว์

คุณปล่อยก๊าซแรกสู่การเดินทางได้โดยโทรหา first() ช่วงเวลานี้ รอจนกว่าจะได้รับรายการแรกแล้วจึงส่งการยกเลิก ไปยังผู้ผลิต

@Test
fun myRepositoryTest() = runTest {
    // Given a repository that combines values from two data sources:
    val repository = MyRepository(fakeSource1, fakeSource2)

    // When the repository emits a value
    val firstItem = repository.counter.first() // Returns the first item in the flow

    // Then check it's the expected item
    assertEquals(ITEM_1, firstItem)
}

หากการทดสอบต้องตรวจสอบค่าหลายค่า การเรียกใช้ toList() จะทำให้เกิดโฟลว์ เพื่อรอให้แหล่งที่มาปล่อยค่าทั้งหมด จากนั้นแสดงผลค่าเหล่านั้นเป็น รายการ วิธีนี้ใช้ได้กับสตรีมข้อมูลที่จำกัดเท่านั้น

@Test
fun myRepositoryTest() = runTest {
    // Given a repository with a fake data source that emits ALL_MESSAGES
    val messages = repository.observeChatMessages().toList()

    // When all messages are emitted then they should be ALL_MESSAGES
    assertEquals(ALL_MESSAGES, messages)
}

สำหรับสตรีมข้อมูลที่ต้องมีคอลเล็กชันรายการที่ซับซ้อนมากขึ้น หรือไม่แสดงผล คุณสามารถใช้ Flow API เพื่อเลือกและเปลี่ยนรูปแบบไอเทมได้ในจำนวนจำกัด รายการ ตัวอย่างเช่น

// Take the second item
outputFlow.drop(1).first()

// Take the first 5 items
outputFlow.take(5).toList()

// Takes the first item verifying that the flow is closed after that
outputFlow.single()

// Finite data streams
// Verify that the flow emits exactly N elements (optional predicate)
outputFlow.count()
outputFlow.count(predicate)

การเก็บรวบรวมข้อมูลอย่างต่อเนื่องระหว่างการทดสอบ

การรวบรวมขั้นตอนโดยใช้ toList() ตามที่เห็นในตัวอย่างก่อนหน้านี้ใช้ collect()ภายในและระงับจนกว่ารายการผลลัพธ์ทั้งหมดจะพร้อมแสดง ส่งคืนแล้ว

เพื่อแทรกการดำเนินการที่ทำให้โฟลว์แสดงค่าและการยืนยัน ค่าที่มีการปล่อยออกมา คุณสามารถรวบรวมค่าจากขั้นตอนหนึ่งๆ ได้อย่างต่อเนื่องระหว่าง การทดสอบ

ตัวอย่างเช่น ทำแบบทดสอบ Repository ต่อไปนี้เพื่อรับการทดสอบ และ ที่มาพร้อมกับการใช้งานแหล่งข้อมูลปลอมที่มีเมธอด emit ในการ สร้างค่าแบบไดนามิกระหว่างการทดสอบ ดังนี้

class Repository(private val dataSource: DataSource) {
    fun scores(): Flow<Int> {
        return dataSource.counts().map { it * 10 }
    }
}

class FakeDataSource : DataSource {
    private val flow = MutableSharedFlow<Int>()
    suspend fun emit(value: Int) = flow.emit(value)
    override fun counts(): Flow<Int> = flow
}

เมื่อใช้ข้อมูลปลอมนี้ในการทดสอบ คุณสามารถสร้างโครูทีนสะสมที่จะ ได้รับค่าจาก Repository อย่างต่อเนื่อง ในตัวอย่างนี้ เราจะ รวบรวมเนื้อหาไว้เป็นรายการ และดำเนินการยืนยันในเนื้อหา

@Test
fun continuouslyCollect() = runTest {
    val dataSource = FakeDataSource()
    val repository = Repository(dataSource)

    val values = mutableListOf<Int>()
    backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
        repository.scores().toList(values)
    }

    dataSource.emit(1)
    assertEquals(10, values[0]) // Assert on the list contents

    dataSource.emit(2)
    dataSource.emit(3)
    assertEquals(30, values[2])

    assertEquals(3, values.size) // Assert the number of items collected
}

เนื่องจากโฟลว์ที่ Repository เปิดเผยที่นี่ไม่มีวันเสร็จสมบูรณ์ toList ที่เก็บรวบรวมไว้ก็จะไม่กลับมาอีก กำลังเริ่มเก็บโครูทีนใน TestScope.backgroundScope ช่วยให้แน่ใจว่าโครูทีนถูกยกเลิกก่อนสิ้นสุดการทดสอบ หรือไม่เช่นนั้น runTest จะรออย่างต่อเนื่องจนเสร็จสิ้น ซึ่งทำให้การทดสอบหยุดลง และล้มเหลวในที่สุด

สังเกตวิธี UnconfinedTestDispatcher ใช้สำหรับเก็บค่าโครูทีนที่นี่ วิธีนี้ช่วยให้มั่นใจว่าการรวบรวม เปิดตัว Coroutine ด้วยความตั้งใจและพร้อมรับค่าหลังจากวันที่ launch ที่เกินออกมา

การใช้กังหัน

Turbine ของบุคคลที่สาม ไลบรารีมี API ที่ใช้งานได้สะดวกสำหรับการสร้างโครูทีนในการรวบรวม รวมถึง เช่นเดียวกับฟีเจอร์อำนวยความสะดวกอื่นๆ สำหรับการทดสอบโฟลว์

@Test
fun usingTurbine() = runTest {
    val dataSource = FakeDataSource()
    val repository = Repository(dataSource)

    repository.scores().test {
        // Make calls that will trigger value changes only within test{}
        dataSource.emit(1)
        assertEquals(10, awaitItem())

        dataSource.emit(2)
        awaitItem() // Ignore items if needed, can also use skip(n)

        dataSource.emit(3)
        assertEquals(30, awaitItem())
    }
}

โปรดดู เอกสารประกอบของlibrary เกี่ยวกับ รายละเอียดเพิ่มเติม

StateFlow ของการทดสอบ

StateFlow เป็นที่สังเกตได้ ซึ่งสามารถเก็บรวบรวมเพื่อดูค่าที่เก็บไว้เมื่อเวลาผ่านไป สตรีม โปรดทราบว่าสตรีมของค่านี้จะมีการปนกัน ซึ่งหมายความว่าถ้า ได้รับการตั้งค่าใน StateFlow อย่างรวดเร็ว ตัวรวบรวมของ StateFlow ดังกล่าวจะไม่ คุณจะได้รับค่ากลางทั้งหมด เฉพาะค่าล่าสุดเท่านั้น

ในการทดสอบ หากคํานึงถึงการผสม คุณจะรวบรวมค่า StateFlow ได้ เช่นเดียวกับที่รวบรวมขั้นตอนอื่นๆ ได้ รวมถึงการใช้ Turbine กำลังพยายามรวบรวม และการยืนยันค่ากลางทั้งหมดอาจให้ผลลัพธ์ที่น่าพอใจในสถานการณ์การทดสอบบางอย่าง

แต่โดยทั่วไปเราแนะนำให้ปฏิบัติต่อ StateFlow ในฐานะเจ้าของข้อมูลและ ยืนยันในพร็อพเพอร์ตี้ value แทน วิธีนี้จะทำให้การทดสอบตรวจสอบ ของออบเจ็กต์ ณ ช่วงเวลาหนึ่งๆ และไม่ได้ขึ้นอยู่กับว่า การผันผวนเกิดขึ้น

ตัวอย่างเช่น ใช้ ViewModel ที่รวบรวมค่าจาก Repository และ เพื่อแสดง UI ใน StateFlow:

class MyViewModel(private val myRepository: MyRepository) : ViewModel() {
    private val _score = MutableStateFlow(0)
    val score: StateFlow<Int> = _score.asStateFlow()

    fun initialize() {
        viewModelScope.launch {
            myRepository.scores().collect { score ->
                _score.value = score
            }
        }
    }
}

การติดตั้งใช้งานปลอมสำหรับ Repository นี้อาจมีลักษณะเช่นนี้

class FakeRepository : MyRepository {
    private val flow = MutableSharedFlow<Int>()
    suspend fun emit(value: Int) = flow.emit(value)
    override fun scores(): Flow<Int> = flow
}

เมื่อทดสอบ ViewModel ด้วยของปลอมนี้ คุณสามารถปล่อยค่าจากของปลอมเป็น เรียกใช้การอัปเดตใน StateFlow ของ ViewModel แล้วยืนยันใน value:

@Test
fun testHotFakeRepository() = runTest {
    val fakeRepository = FakeRepository()
    val viewModel = MyViewModel(fakeRepository)

    assertEquals(0, viewModel.score.value) // Assert on the initial value

    // Start collecting values from the Repository
    viewModel.initialize()

    // Then we can send in values one by one, which the ViewModel will collect
    fakeRepository.emit(1)
    assertEquals(1, viewModel.score.value)

    fakeRepository.emit(2)
    fakeRepository.emit(3)
    assertEquals(3, viewModel.score.value) // Assert on the latest value
}

การทำงานร่วมกับ StateFlows ที่สร้างโดย StateIn

ในส่วนก่อนหน้านี้ ViewModel ใช้ MutableStateFlow เพื่อจัดเก็บ ค่าล่าสุดที่เกิดจากโฟลว์จาก Repository นี่คือรูปแบบที่พบบ่อย มักติดตั้งด้วยวิธีที่เรียบง่ายขึ้นโดยใช้ stateIn ซึ่งจะแปลงการไหลเย็นเป็น StateFlow ที่ร้อน

class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
    val score: StateFlow<Int> = myRepository.scores()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}

โอเปอเรเตอร์ stateIn มีพารามิเตอร์ SharingStarted ซึ่งเป็นตัวกำหนดเวลา อินสแตนซ์ก็จะพร้อมใช้งานและเริ่มใช้ขั้นตอนเบื้องหลัง ตัวเลือกต่างๆ เช่น มีการใช้ SharingStarted.Lazily และ SharingStarted.WhileSubsribed บ่อย ใน ViewModels

แม้ว่าคุณจะยืนยันที่ value ของ StateFlow ในการทดสอบ เพื่อสร้างเครื่องมือรวบรวม ซึ่งอาจเป็นผู้รวบรวมที่ว่างเปล่า

@Test
fun testLazilySharingViewModel() = runTest {
    val fakeRepository = HotFakeRepository()
    val viewModel = MyViewModelWithStateIn(fakeRepository)

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

    assertEquals(0, viewModel.score.value) // Can assert initial value

    // Trigger-assert like before
    fakeRepository.emit(1)
    assertEquals(1, viewModel.score.value)

    fakeRepository.emit(2)
    fakeRepository.emit(3)
    assertEquals(3, viewModel.score.value)
}

แหล่งข้อมูลเพิ่มเติม