Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unit test for ViewModel and LiveData

Unit test for ViewModel and LiveData

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

Hiroyuki Kusu

February 07, 2019
Tweet

More Decks by Hiroyuki Kusu

Other Decks in Programming

Transcript

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

    ͷςετ • ςετϑϨϯυϦʔͳ ViewModel ͷઃܭ ໨࣍
  2. • 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"
  3. • 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ʣ
  4. ςετର৅ͷ 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) } } } }
  5. 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) } } } }
  6. class ItemRepository(private val itemService: ItemService) { suspend fun getItemList(): List<Item>

    { return itemService.items(page = 1, perPage = 10).await() } } ItemRepository
  7. 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) } } } }
  8. 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
  9. 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) } } } }
  10. 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 ܕʹ
  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 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 Λىಈ
  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) } } } } Ұ୴ʮLoadingʯঢ়ଶʹ
  14. 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 Ҏ߱
  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) } } } } IOεϨου΁੾Γସ͑
  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) } } } } Repository ͔Β Item ͷ List Λऔಘ
  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) } } } } Τϥʔ͕ൃੜ͠ͳ͔ͬͨ৔߹ ( it ͸ List<Item> ܕ )
  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) } } } } Τϥʔ͕ൃੜͨ͠৔߹ ( it ͸ Throwable ܕ )
  19. 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 Λ؍ଌ͠ɺߋ৽͕͋ͬͨΒঢ়ଶʹԠͯ͡ͳΜΒ͔ͷॲཧΛ͢Δ
  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) */ } } } } ※ LiveData ʹؔ͢Δίʔυ͸Ұ୴ίϝϯτΞ΢τ ͜ͷ෦෼ͷςετ
  21. 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 ͷϞοΫΠϯελϯεΛ࡞੒
  22. 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ͷΠϯελϯεΛ࡞੒
  23. 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() } } } }) ϞοΫΠϯελϯεͷϝιου΋ϞοΫ
  24. 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() } } } }) ςετର৅ͷϝιουΛ࣮ߦ
  25. 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() } } } }) ϞοΫΠϯελϯεͷϝιου͕ظ଴Ͳ͓Γݺ͹Ε͔ͨΛݕূ
  26. 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 ͷ࣮ߦεϨουΛ੾Γସ͑
  27. 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) } // … ֦ுؔ਺Λఆٛ
  28. 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) */ } } } ← ͳʹ͔͠Βͷதஅ͕͋Δ৔߹.. ͨͩ͠..
  29. 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() } } } }) ← ׬ྃΛ଴ͯͯͳ͍ ← ଴ͨͣʹ࣮ߦ͞Εͯ͠·͏
  30. 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 ׬ྃΛݕ஌͢Δखஈ͕ͳ͍..
  31. 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 ͯ͠தஅؔ਺ʹ ← ੺࿮ͷॲཧ͕ऴΘ͔ͬͯΒ࣮ߦ͞ΕΔ
  32. 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 ͷίϝϯτΞ΢τΛ֎ͯ͠ݩʹ
  33. 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"(ࠓճͷ؀ڥͰ͸ະಋೖ)
  34. 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); } }
  35. 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 ༻ʹಉ͡Α͏ͳ΋ͷΛ༻ҙ
  36. object MainViewModelTest : Spek({ applyTestDispatcher() applyInstantTaskExecutor() val itemRepository:ItemRepository by memoized

    { mockk<ItemRepository>() } val targetViewModel:MainViewModel by memoized { MainViewModel(itemRepository) } // ...
  37. 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 ͷϞοΫΛ࡞੒
  38. 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 Λ؍ଌ
  39. 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())) } } } }) ςετର৅ͷϝιουΛ࣮ߦ
  40. 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 ͕ߋ৽͞Ε͔ͨΛݕূ
  41. 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 } ֦ுؔ਺Λఆٛ
  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) } } }
  43. 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 ΁ࠩ͠ࠐΊΔ
  44. ViewModel ͷ෼ׂΛݕ౼ ը໘(Activity/Fragment) ̍ͭʹ ViewModel ̍ͭͱ͍͏੍໿͕ಛʹ͋ΔΘ͚Ͱ͸ͳ͍ ※ ͨͩ͠ ViewModel Ͳ͏͠ͷ࿈ಈ͸ΑΓෳࡶʹͳͬͯ͠·͏ͷͰ΍Βͳ͍͜ͱ

    class MainActivity : AppCompatActivity() { private val mainViewModel: MainViewModel by viewModel() private val someViewModel: SomeViewModel by viewModel() // ... (KOINͰDI͢Δྫ)
  45. 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 = " " ม׵
  46. 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) } } }
  47. 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) } } }
  48. 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 ϝιουΛςετ
  49. 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()) { // ... } } } ผϝιουɾϓϩύςΟ΁੾Γग़͠
  50. object MainViewModelTest : Spek({ // ... val targetViewModel:MainViewModel by memoized

    { // MainViewModel(itemRepository) spyk(MainViewModel(itemRepository)) } describe("...") { it("...") { every { targetViewModel.isMember() } returns false // ... } } }) ViewModel ࣗମ ͷϝιουΛஔ͖׵͑
  51. // 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() = ...
  52. 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 } } } ผͷΫϥε΁੾Γग़͠
  53. 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 } } }
  54. class MainViewModelTest : Spek({ applyInstantTaskExecutor() applyTestDispatcher() val getItemListUseCase: GetItemListUseCase by

    memoized { mockk<GetItemListUseCase>() } val targetViewModel: MainViewModel by memoized { MainViewModel(getItemListUseCase) } // ... ViewModelͷςετ࣌ʹ͸ϞοΫͯ͠͠·͏
  55. Spek ͷ memoized() ςετέʔεຖʹΠϯελϯε͕ੜ੒͞Εͯศར (ଞͷςετέʔεͷ෭࡞༻Λड͚ͳ͍) object MainViewModelTest : Spek({ val

    itemRepository: ItemRepository by memoized { mockk<ItemRepository>() } val targetViewModel: MainViewModel by memoized { MainViewModel(itemRepository) } describe("MainViewModel#loadItemList()") { // …
  56. 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 ͸ಈ͔ͤͳ͍
  57. 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 Λઃܭ͕๬·͍͠
  58. 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 Λࢀߟʹ͠·ͨ͠
  59. 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() }
  60. 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 ͷॳظॲཧΛݺ ͼग़ͨ͠ํ͕ແ೉͔΋͠Εͳ͍
  61. 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)) } } // ...
  62. 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" }
  63. END