Unit test for ViewModel and LiveData

Unit test for ViewModel and LiveData

DroidKaigi 2019 で発表した資料です。

D8281434c0409ba2051cd3f7590e4c2f?s=128

Hiroyuki Kusu

February 07, 2019
Tweet

Transcript

  1. Unit test for ViewModel and LiveData 2019.02.07 DroidKaigi 2019 DAY.01

    Hiroyuki Kusu ( @hkusu_ )
  2. About me Hiroyuki Kusu

  3. • લఏ • ViewModel ͷςετํ๏ • Coroutine ͷςετ • LiveData

    ͷςετ • ςετϑϨϯυϦʔͳ ViewModel ͷઃܭ ໨࣍
  4. લఏ

  5. • Kotlin 1.3.20 • Coroutines 1.1.1 • ViewModel, LiveData 2.1.0-alpha01

    ؀ڥʢViewModel & LiveDataʣ implementation "androidx.lifecycle:lifecycle-extensions:2.1.0-alpha01" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-alpha01" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.1.0-alpha01" implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0-alpha01"
  6. • Spek 2.0.0-rc.1 • android-junit5 1.3.2.0 • MockK 1.9 •

    kotlinx-coroutines-test 1.1.0 • Truth 0.42 (ࠓճ͸͋·Γ࢖Θͳ͍) • Android Studio 3.3 • Spek Framework 2.0.0-rc.1.180+b8533a4-Studio3.3 (Android Studio ͷ plugin) ؀ڥʢLocal unit testʣ
  7. ࿩͞ͳ͍͜ͱ • Spek ͷ࢖͍ํ • https://spekframework.org Λࢀর • MockK ͷ࢖͍ํ

    • https://mockk.io Λࢀর
  8. ૝ఆ͢ΔΞϓϦέʔγϣϯΞʔΩςΫνϟ https://developer.android.com/jetpack/docs/guide?hl=ja

  9. https://developer.android.com/jetpack/docs/guide?hl=ja ͷਤΛҾ༻ ࠓճ͸ Coroutine Λ࢖͏

  10. ςετର৅ͷ ViewModel class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() {

    private val _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } }
  11. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } }
  12. class ItemRepository(private val itemService: ItemService) { suspend fun getItemList(): List<Item>

    { return itemService.items(page = 1, perPage = 10).await() } } ItemRepository
  13. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } }
  14. sealed class Status<out T> { object Loading : Status<Nothing>() data

    class Success<T>(val data: T) : Status<T>() data class Failure(val throwable: Throwable) : Status<Nothing>() } Loading(௨৴த) Failure(ࣦഊ) Success(੒ޭ) Status
  15. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } }
  16. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } "androidx.lifecycle:lifecycle-livedata-ktx:2.1.0-alpha01" ಉҰΠϕϯτΛഉআ͠ͳ͕Β LiveData ܕʹ
  17. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } }
  18. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-alpha01" Coroutine Λىಈ
  19. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } Ұ୴ʮLoadingʯঢ়ଶʹ
  20. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } runCatching(Resultܕ)͸ Kotlin 1.3 Ҏ߱
  21. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } IOεϨου΁੾Γସ͑
  22. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } Repository ͔Β Item ͷ List Λऔಘ
  23. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } Τϥʔ͕ൃੜ͠ͳ͔ͬͨ৔߹ ( it ͸ List<Item> ܕ )
  24. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } Τϥʔ͕ൃੜͨ͠৔߹ ( it ͸ Throwable ܕ )
  25. class MainActivity : AppCompatActivity() { private val mainViewModel: MainViewModel by

    viewModel() // KOIN Λར༻ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mainViewModel.loadItemList() mainViewModel.itemListStatus.observe( owner: this, Observer { when (it) { is Status.Loading -> ... is Status.Success -> ... is Status.Failure -> ... } }) } // ... Activity ଆ LiveData Λ؍ଌ͠ɺߋ৽͕͋ͬͨΒঢ়ଶʹԠͯ͡ͳΜΒ͔ͷॲཧΛ͢Δ
  26. ViewModel ͷςετํ๏ (Coroutine ͷςετ)

  27. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { // private

    val _itemListStatus = MutableLiveData<Status<List<Item>>>() // val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { // _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { /* _itemListStatus.value = Status.Success(it) */ } .onFailure { /* _itemListStatus.value = Status.Failure(it) */ } } } } ※ LiveData ʹؔ͢Δίʔυ͸Ұ୴ίϝϯτΞ΢τ ͜ͷ෦෼ͷςετ
  28. object MainViewModelTest : Spek({ val itemRepository:ItemRepository by memoized { mockk<ItemRepository>()

    } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { coEvery { itemRepository.getItemList() } returns listOf() // ۭͷϦετΛฦ͢ targetViewModel.loadItemList() coVerify(exactly = 1) { itemRepository.getItemList() } } } }) ItemRepository ͷϞοΫΠϯελϯεΛ࡞੒
  29. object MainViewModelTest : Spek({ val itemRepository:ItemRepository by memoized { mockk<ItemRepository>()

    } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { coEvery { itemRepository.getItemList() } returns listOf() // ۭͷϦετΛฦ͢ targetViewModel.loadItemList() coVerify(exactly = 1) { itemRepository.getItemList() } } } }) ςετର৅ͷViewMoelͷΠϯελϯεΛ࡞੒
  30. object MainViewModelTest : Spek({ val itemRepository:ItemRepository by memoized { mockk<ItemRepository>()

    } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { coEvery { itemRepository.getItemList() } returns listOf() // ۭͷϦετΛฦ͢ targetViewModel.loadItemList() coVerify(exactly = 1) { itemRepository.getItemList() } } } }) ϞοΫΠϯελϯεͷϝιου΋ϞοΫ
  31. object MainViewModelTest : Spek({ val itemRepository:ItemRepository by memoized { mockk<ItemRepository>()

    } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { coEvery { itemRepository.getItemList() } returns listOf() // ۭͷϦετΛฦ͢ targetViewModel.loadItemList() coVerify(exactly = 1) { itemRepository.getItemList() } } } }) ςετର৅ͷϝιουΛ࣮ߦ
  32. object MainViewModelTest : Spek({ val itemRepository:ItemRepository by memoized { mockk<ItemRepository>()

    } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { coEvery { itemRepository.getItemList() } returns listOf() // ۭͷϦετΛฦ͢ targetViewModel.loadItemList() coVerify(exactly = 1) { itemRepository.getItemList() } } } }) ϞοΫΠϯελϯεͷϝιου͕ظ଴Ͳ͓Γݺ͹Ε͔ͨΛݕূ
  33. None
  34. object MainViewModelTest : Spek({ beforeEachTest { Dispatchers.setMain(Dispatchers.Unconfined) } afterEachTest {

    Dispatchers.resetMain() } val itemRepository:ItemRepository by memoized { mockk<ItemRepository>() } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { coEvery { itemRepository.getItemList() } returns listOf() targetViewModel.loadItemList() coVerify(exactly = 1) { itemRepository.getItemList() } } } }) "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.1.0" Coroutine ͷ࣮ߦεϨουΛ੾Γସ͑
  35. fun GroupBody.applyTestDispatcher() { beforeEachTest { Dispatchers.setMain(Dispatchers.Unconfined) } afterEachTest { Dispatchers.resetMain()

    } } object MainViewModelTest : Spek({ applyTestDispatcher() val itemRepository:ItemRepository by memoized { mockk<ItemRepository>() } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } // … ֦ுؔ਺Λఆٛ
  36. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { // private

    val _itemListStatus = MutableLiveData<Status<List<Item>>>() // val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { // _itemListStatus.value = Status.Loading delay(1000) runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { /* _itemListStatus.value = Status.Success(it) */ } .onFailure { /* _itemListStatus.value = Status.Failure(it) */ } } } ← ͳʹ͔͠Βͷதஅ͕͋Δ৔߹.. ͨͩ͠..
  37. None
  38. object MainViewModelTest : Spek({ applyTestDispatcher() val itemRepository:ItemRepository by memoized {

    mockk<ItemRepository>() } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { coEvery { itemRepository.getItemList() } returns listOf() targetViewModel.loadItemList() coVerify(exactly = 1) { itemRepository.getItemList() } } } }) ← ׬ྃΛ଴ͯͯͳ͍ ← ଴ͨͣʹ࣮ߦ͞Εͯ͠·͏
  39. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { // private

    val _itemListStatus = MutableLiveData<Status<List<Item>>>() // val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() = viewModelScope.launch { // _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { /* _itemListStatus.value = Status.Success(it) */ } .onFailure { /* _itemListStatus.value = Status.Failure(it) */ } } } class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { // private val _itemListStatus = MutableLiveData<Status<List<Item>>>() // val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { // _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { /* _itemListStatus.value = Status.Success(it) */ } .onFailure { /* _itemListStatus.value = Status.Failure(it) */ } } } } ← return Unit ← return Job ׬ྃΛݕ஌͢Δखஈ͕ͳ͍..
  40. object MainViewModelTest : Spek({ applyTestDispatcher() val itemRepository:ItemRepository by memoized {

    mockk<ItemRepository>() } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { coEvery { itemRepository.getItemList() } returns listOf() runBlocking { targetViewModel.loadItemList().join() } coVerify(exactly = 1) { itemRepository.getItemList() } } } }) Job Λ join ͯ͠தஅؔ਺ʹ ← ੺࿮ͷॲཧ͕ऴΘ͔ͬͯΒ࣮ߦ͞ΕΔ
  41. ViewModel ͷςετํ๏ (LiveData ͷςετ)

  42. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() = viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } ※ LiveData ͷίϝϯτΞ΢τΛ֎ͯ͠ݩʹ
  43. None
  44. public class InstantTaskExecutorRule extends TestWatcher { @Override protected void starting(Description

    description) { super.starting(description); ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { @Override public void executeOnDiskIO(Runnable runnable) { runnable.run(); } @Override public void postToMainThread(Runnable runnable) { runnable.run(); } @Override public boolean isMainThread() { return true; } }); } @Override protected void finished(Description description) { super.finished(description); ArchTaskExecutor.getInstance().setDelegate(null); } } ΋͠ JUnit ϕʔεͷςετͰ͋Ε͹.. "androidx.arch.core:core-testing:2.0.0"(ࠓճͷ؀ڥͰ͸ະಋೖ)
  45. public class InstantTaskExecutorRule extends TestWatcher { @Override protected void starting(Description

    description) { super.starting(description); ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { @Override public void executeOnDiskIO(Runnable runnable) { runnable.run(); } @Override public void postToMainThread(Runnable runnable) { runnable.run(); } @Override public boolean isMainThread() { return true; } }); } @Override protected void finished(Description description) { super.finished(description); ArchTaskExecutor.getInstance().setDelegate(null); } }
  46. private object InstantTaskExecutor : TaskExecutor() { override fun executeOnDiskIO(runnable: Runnable)

    { runnable.run() } override fun postToMainThread(runnable: Runnable) { runnable.run() } override fun isMainThread(): Boolean = true } fun GroupBody.applyInstantTaskExecutor() { beforeEachTest { ArchTaskExecutor.getInstance().setDelegate(InstantTaskExecutor) } afterEachTest { ArchTaskExecutor.getInstance().setDelegate(null) } } Spek ༻ʹಉ͡Α͏ͳ΋ͷΛ༻ҙ
  47. object MainViewModelTest : Spek({ applyTestDispatcher() applyInstantTaskExecutor() val itemRepository:ItemRepository by memoized

    { mockk<ItemRepository>() } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } // ...
  48. object MainViewModelTest : Spek({ // ... describe("...") { it("...") {

    coEvery { itemRepository.getItemList() } returns listOf() val observer:Observer<Status<List<Item>>> = mockk<Observer<Status<List<Item>>>> { every { onChanged(any()) } just Runs } targetViewModel.itemListStatus.observeForever(observer) runBlocking { targetViewModel.loadItemList().join() } coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(listOf())) } } } }) Observer ͷϞοΫΛ࡞੒
  49. object MainViewModelTest : Spek({ // ... describe("...") { it("...") {

    coEvery { itemRepository.getItemList() } returns listOf() val observer:Observer<Status<List<Item>>> = mockk<Observer<Status<List<Item>>>> { every { onChanged(any()) } just Runs } targetViewModel.itemListStatus.observeForever(observer) runBlocking { targetViewModel.loadItemList().join() } coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(listOf())) } } } }) ςετର৅ͷ LiveData Λ؍ଌ
  50. object MainViewModelTest : Spek({ // ... describe("...") { it("...") {

    coEvery { itemRepository.getItemList() } returns listOf() val observer:Observer<Status<List<Item>>> = mockk<Observer<Status<List<Item>>>> { every { onChanged(any()) } just Runs } targetViewModel.itemListStatus.observeForever(observer) runBlocking { targetViewModel.loadItemList().join() } coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(listOf())) } } } }) ςετର৅ͷϝιουΛ࣮ߦ
  51. object MainViewModelTest : Spek({ // ... describe("...") { it("...") {

    coEvery { itemRepository.getItemList() } returns listOf() val observer:Observer<Status<List<Item>>> = mockk<Observer<Status<List<Item>>>> { every { onChanged(any()) } just Runs } targetViewModel.itemListStatus.observeForever(observer) runBlocking { targetViewModel.loadItemList().join() } coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(listOf())) } } } }) ظ଴Ͳ͓ΓͷॱͰ LiveData ͕ߋ৽͞Ε͔ͨΛݕূ
  52. object MainViewModelTest : Spek({ // ... describe("...") { it("...") {

    coEvery { itemRepository.getItemList() } returns listOf() val observer:Observer<Status<List<Item>>!> = targetViewModel.itemListStatus.test() runBlocking { targetViewModel.loadItemList().join() } // ... fun <T> LiveData<T>.test(): Observer<T> { val observer:Observer<T> = mockk<Observer<T>>(relaxUnitFun = true) this.observeForever(observer) return observer } ֦ுؔ਺Λఆٛ
  53. ςετϑϨϯυϦʔͳ ViewModel ͷઃܭ

  54. ίϯετϥΫλͰґଘΠϯελϯεΛ౉͢ class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private

    val _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() = viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } }
  55. object MainViewModelTest : Spek({ applyTestDispatcher() applyInstantTaskExecutor() val itemRepository:ItemRepository by memoized

    { mockk<ItemRepository>() } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } describe("...") { it("...") { val itemList:List<Item> = listOf( Item("id1", "title1"), Item("id2", "title2") ) coEvery { itemRepository.getItemList() } returns itemList // ... ϞοΫͯ͠ ViewModel ΁ࠩ͠ࠐΊΔ
  56. ViewModel ͷ෼ׂΛݕ౼ ը໘(Activity/Fragment) ̍ͭʹ ViewModel ̍ͭͱ͍͏੍໿͕ಛʹ͋ΔΘ͚Ͱ͸ͳ͍ ※ ͨͩ͠ ViewModel Ͳ͏͠ͷ࿈ಈ͸ΑΓෳࡶʹͳͬͯ͠·͏ͷͰ΍Βͳ͍͜ͱ

    class MainActivity : AppCompatActivity() { private val mainViewModel: MainViewModel by viewModel() private val someViewModel: SomeViewModel by viewModel() // ... (KOINͰDI͢Δྫ)
  57. Static ϝιου΁੾Γग़͠ data class Item( val id: String, val title:

    String ) data class MainItem( val id: String, val title: String? ) ྫ͑͹.. ItemΫϥεͷ title ϓϩύςΟ͸ API ͷ݁ՌΛ֨ೲ͍ͯ͠ Δ౎߹্ɺۭจࣈྻ ΍ ۭനจࣈྻ ؚ͕·Εͯ͠·͏ ϝΠϯը໘Ͱ͸ͦΕΒ͸ແޮͳσʔλ(= null)ͱͯ͠ѻ͍ ͍ͨ ͱ͍͏έʔε title = "" title = " " ม׵
  58. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() = viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .map { itemList -> itemList.map { item -> MainItem( item.id, if (!item.title.isBlank()) item.title else null ) } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } }
  59. object ItemConverter { fun convert(item: Item): MainItem = item.let {

    MainItem( it.id, if (!it.title.isBlank()) it.title else null ) } } // ... class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() = viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .map { it.map(ItemConverter::convert) } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } }
  60. object MainViewModelTest : Spek({ describe("ItemConverter.convert()") { val testCase:Map<Item, MainItem> =

    mapOf( Item("id1", "title1") to MainItem("id1", "title1"), Item("id2", "") to MainItem("id2", null), Item("id3", " ") to MainItem("id3", null) ) testCase.forEach { inItem, outItem -> it("$inItem is converted to $outItem") { assertThat(MainViewModel.ItemConverter.convert(inItem)).isEqualTo(outItem) } } } Static ϝιουΛςετ
  61. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun isMember(): Boolean { // ... } fun loadItemList() = viewModelScope.launch { if (isMember()) { // ... } } } ผϝιουɾϓϩύςΟ΁੾Γग़͠
  62. object MainViewModelTest : Spek({ // ... val targetViewModel:MainViewModel by memoized

    { // MainViewModel(itemRepository) spyk(MainViewModel(itemRepository)) } describe("...") { it("...") { every { targetViewModel.isMember() } returns false // ... } } }) ViewModel ࣗମ ͷϝιουΛஔ͖׵͑
  63. // public property ͷ৔߹ every { targetViewModel.isSome } returns false

    // private method ͷ৔߹ every { targetViewModel["isSome"]() } returns false // private property ͷ৔߹ every { targetViewModel getProperty "isSome" } returns false ϞοΫʹࠩ͠ସ͍͑ͨ΋ͷ ΛϓϩύςΟɾϝιουʹ͓ͯ͘͠ ݱঢ়ͷ MockK(1.9) Ͱ͸ getter Λఆ͓ٛͯ͘͠ඞཁ͕ ͋Δ໛༷ private val isSome get() = ...
  64. class MainViewModel( private val accountRepository: AccountRepository, private val itemRepository: ItemRepository

    ) : ViewModel() { val itemList = MutableLiveData<List<Item>>() fun loadItemList() = viewModelScope.launch { runCatching { withContext(Dispatchers.IO) { val account: Account = accountRepository.getAccount() when (account.status) { Account.Status.GUEST -> ^withContext itemRepository.getItemList() Account.Status.MEMBER -> ^withContext itemRepository.getItemList(account) } } }.onSuccess { it: List<Item> itemList.value = it } } } ผͷΫϥε΁੾Γग़͠
  65. class GetItemListUseCase( private val accountRepository: AccountRepository, private val itemRepository: ItemRepository

    ) { suspend operator fun invoke(): List<Item> { val account:Account = accountRepository.getAccount() return when (account.status) { Account.Status.GUEST -> itemRepository.getItemList() Account.Status.MEMBER -> itemRepository.getItemList(account) } } } class MainViewModel(private val getItemListUseCase: GetItemListUseCase) : ViewModel() { val itemList = MutableLiveData<List<Item>>() fun loadItemList() = viewModelScope.launch { runCatching { withContext(Dispatchers.IO) { getItemListUseCase() } } .onSuccess { itemList.value = it } } }
  66. class MainViewModelTest : Spek({ applyInstantTaskExecutor() applyTestDispatcher() val getItemListUseCase: GetItemListUseCase by

    memoized { mockk<GetItemListUseCase>() } val targetViewModel: MainViewModel by memoized { MainViewModel(getItemListUseCase) } // ... ViewModelͷςετ࣌ʹ͸ϞοΫͯ͠͠·͏
  67. ςετෆೳͳՕॴΛϞοΫͰஔ͖׵͑Δҝʹ ੾Γग़ͯ͠͠΋͍͍͔΋͠Εͳ͍

  68. ͓ΘΓʹ • ςετ͕͠΍͍͢ ViewModel ͷઃܭ͸ɺ͓ͷͣͱྑ͍ ViewModel ͷઃܭʹͳΔ͜ͱ͕ଟ͍ • ػೳ͕খ͍͞੹຿ʹ෼͔Ε͍ͯΔ •

    ςετ͕͠ʹ͍͘ͱײͨ͡Βɺઃܭʹ໰୊͕͋Δஹީ͔΋͠Εͳ͍
  69. Thank you ! @hkusu_

  70. ༧උεϥΠυ

  71. Spek ͷ memoized() ςετέʔεຖʹΠϯελϯε͕ੜ੒͞Εͯศར (ଞͷςετέʔεͷ෭࡞༻Λड͚ͳ͍) object MainViewModelTest : Spek({ val

    itemRepository: ItemRepository by memoized { mockk<ItemRepository>() } val targetViewModel: MainViewModel by memoized { MainViewModel(itemRepository) } describe("MainViewModel#loadItemList()") { // …
  72. Android తͳґଘ͕͋Δ৔߹ import android.content.Context class MainViewModel( private val itemRepository: ItemRepository,

    private val applicationContext: Context ) : ViewModel() { fun loadSome() = viewModelScope.launch { val message = applicationContext.getString(R.string.message) // ... } } ※ ݱόʔδϣϯͰ͸ Spek ্Ͱ Robolectric ͸ಈ͔ͤͳ͍
  73. object MainViewModelTest : Spek({ applyTestDispatcher() applyInstantTaskExecutor() val itemRepository:ItemRepository by memoized

    { mockk<ItemRepository>() } val applicationContext by memoized { mockk<Context> { every { getString(any()) } returns "it is mocked string" } } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository, applicationContext) } // ... ϞοΫͯ͠͠·͏ ͱ͸͍͑ Android తͳґଘΛؚ·ͳ͍Α͏ʹ ViewModel Λઃܭ͕๬·͍͠
  74. ը໘ભҠ౳ͷΠϕϯτ ௨ৗͷ LiveData ͩͱ࠷ޙͷ஋Λอ࣋ͯ͠͠·͏ ը໘ͷ࠶ੜ੒Ͱ࠶ Observe ͨ͠ࡍʹΠϕϯτ͕ൃՐͯ͠͠·͏

  75. class SingleLiveEvent<T> : LiveData<T>() { private val pending = AtomicBoolean(false)

    @MainThread override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { if (hasActiveObservers()) { Log.w("SingleLiveEvent", "Multiple observers registered" + " but only one will be notified of changes.") } super.observe(owner, Observer { if (pending.compareAndSet(true, false)) { observer.onChanged(it) } }) } @MainThread public override fun setValue(value: T?) { pending.set(true) super.setValue(value) } } https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/ src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java Λࢀߟʹ͠·ͨ͠
  76. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val

    _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() private val _uiEvent = SingleLiveEvent<MainEvent>() val uiEvent = _uiEvent as LiveData<MainEvent> fun loadItemList() = viewModelScope.launch { this: CoroutineScope _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } fun onItemClicked(position: Int) { _uiEvent.value = MainEvent.NavigateToDetail(position) } } // ... sealed class MainEvent { data class NavigateToDetail(val position: Int) : MainEvent() data class ShowToast(val message: String) : MainEvent() }
  77. init() Ͱ Coroutine Λىಈ͢ΔͷΛආ͚Δʁ class MainViewModel(private val itemRepository: ItemRepository) :

    ViewModel() { private val _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() init { loadItemList() } fun loadItemList() = viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } ςετίʔυଆͰ init {} ͷ׬ྃΛ஌Δखஈ͕ແ͍.. ςετͷ͠΍͢͞Λ༏ઌ͢ΔͳΒ onCreate() ౳Ͱ ViewModel ͷॳظॲཧΛݺ ͼग़ͨ͠ํ͕ແ೉͔΋͠Εͳ͍
  78. Coroutine ͷΤϥʔͷςετ object MainViewModelTest : Spek({ // ... describe(“...”) {

    it(“...”) { val throwable = Throwable() coEvery { itemRepository.getItemList() } throws throwable val observer = targetViewModel.itemListStatus.test() runBlocking { targetViewModel.loadItemList().join() } coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Failure(throwable)) } } // ...
  79. Local unit test ͷ؀ڥߏங buildscript { dependencies { classpath "de.mannodermaus.gradle.plugins:android-junit5:1.3.2.0"

    } } apply plugin: "de.mannodermaus.android-junit5" android { testOptions { junitPlatform { filters { engines { include 'spek2' } } } } } dependencies { testImplementation "com.google.truth:truth:0.42" testImplementation "io.mockk:mockk:1.9" testImplementation "org.spekframework.spek2:spek-dsl-jvm:2.0.0-rc.1" testImplementation "org.spekframework.spek2:spek-runner-junit5:2.0.0-rc.1" testImplementation "org.jetbrains.kotlin:kotlin-reflect:1.3.20" // Spek requires testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.1.0" }
  80. Spek Framework plugin

  81. END