Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

● Use case with ViewModel & Repo ● Testing Repo & ViewModel ● Best practices for unit testing

Slide 3

Slide 3 text

ViewModel Repository

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

interface ApiService { @GET("/users/{id}") suspend fun userDetails(@Path("id") id: Int): UserDetails }

Slide 6

Slide 6 text

class UserRepository(val apiService: ApiService) { fun userDetails(id: Int): Flow { } }

Slide 7

Slide 7 text

fun userDetails(id: Int): Flow { } Coroutines Library fun flow( block: suspend FlowCollector.() .> Unit ): Flow

Slide 8

Slide 8 text

fun userDetails(id: Int): Flow { } Coroutines Library interface FlowCollector { suspend fun emit(value: T) }

Slide 9

Slide 9 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) } }

Slide 10

Slide 10 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) } }

Slide 11

Slide 11 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) } } Coroutines Library Dispatcher - Control scheduling threads - Default, IO, Main, etc...

Slide 12

Slide 12 text

class UserRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher ) { fun userDetails(id: Int): Flow { return flow { val users = apiService.userDetails(id) emit(Result.success(users)) }.flowOn(dispatcher) }

Slide 13

Slide 13 text

class UserRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher ) { fun userDetails(id: Int): Flow { return flow { val users = apiService.userDetails(id) emit(Result.success(users)) }.flowOn(dispatcher) }

Slide 14

Slide 14 text

ViewModel Repository

Slide 15

Slide 15 text

class UserDetailViewModel : ViewModel { }

Slide 16

Slide 16 text

class UserDetailViewModel : ViewModel { val scope = CoroutineScope(Dispatchers.Main) } Coroutines Library fun CoroutineScope(context: CoroutineContext)

Slide 17

Slide 17 text

class UserDetailViewModel : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { } }

Slide 18

Slide 18 text

class UserDetailViewModel(val repository: UserRepository) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { } }

Slide 19

Slide 19 text

class UserDetailViewModel(val repository: UserRepository) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) } }

Slide 20

Slide 20 text

class UserDetailViewModel(val repository: UserRepository) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) } } Coroutines Library fun Flow.collect( action: suspend (value: T) .> Unit )

Slide 21

Slide 21 text

class UserDetailViewModel(val repository: UserRepository) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> } } }

Slide 22

Slide 22 text

class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> stateManager.dispatchState(result) } } }

Slide 23

Slide 23 text

class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> stateManager.dispatchState(result) } } }

Slide 24

Slide 24 text

class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ): ViewModel { { val scope = CoroutineScope(Dispatchers.Main) } Architecture Components val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? = this.getTag(JOB_KEY) return scope }

Slide 25

Slide 25 text

ViewModel Repository

Slide 26

Slide 26 text

1. Flow Collection 2. Error handling 3. Retries with Delays (Advanced)

Slide 27

Slide 27 text

testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.3' ● Experimental ● runBlockingTest ● Test Coroutine Scope ● Test Coroutine Dispatcher

Slide 28

Slide 28 text

1. Flow emits Success 2. Flow emits Error 3. Retries with delay (Advanced)

Slide 29

Slide 29 text

class UserRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher ) { fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) } }

Slide 30

Slide 30 text

@Test fun `should get users details on success`() = runBlocking { } Coroutines Library RunBlocking - Blocks untils coroutine completes

Slide 31

Slide 31 text

@Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock() }

Slide 32

Slide 32 text

@Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock() } API Service suspend fun userDetails(id: Int): UserDetails

Slide 33

Slide 33 text

@Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock() { on { userDetails(1) } doReturn userDetails } }

Slide 34

Slide 34 text

@Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock() { on { userDetails(1) } doReturn userDetails } } Mockito-Kotlin fun on(methodCall: T.() .> R)

Slide 35

Slide 35 text

@Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock() } Mockito-Kotlin fun onBlocking(m: suspend T.() .> R) { return runBlocking { Mockito.`when`(mock.m()) } }

Slide 36

Slide 36 text

@Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock() { onBlocking { userDetails(1) } doReturn userDetails } }

Slide 37

Slide 37 text

@Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock() { onBlocking { userDetails(1) } doReturn userDetails } val dispatcher = TestCoroutineDispatcher() val repository = UserRepository(userService, dispatcher) val flow = repository.getUserDetails(id = 1) }

Slide 38

Slide 38 text

@Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock() { onBlocking { userDetails(1) } doReturn userDetails } val dispatcher = TestCoroutineDispatcher() val repository = UserRepository(userService, dispatcher) val flow = repository.getUserDetails(id = 1) val result = flow.single() result: Result result.isSuccess.assertTrue() }

Slide 39

Slide 39 text

1. Flow emits Success 2. Flow emits Error 3. Retries with delay (Advanced)

Slide 40

Slide 40 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) }

Slide 41

Slide 41 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) } ViewModel scope.launch { val flow: = userRepository.getUserDetails(id = 1) flow.collect { } }

Slide 42

Slide 42 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) } Coroutines-Library fun Flow.catch( action: suspend FlowCollector.(t: Throwable) .> Unit ): Flow

Slide 43

Slide 43 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 44

Slide 44 text

@Test fun `should get error for user details`() = runBlocking { val apiService = mock { onBlocking { userDetails(1) } doAnswer { throw IOException() } } }

Slide 45

Slide 45 text

@Test fun `should get error for user details`() = runBlocking { val apiService = mock { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val repository = UserRepository(apiService, dispatcher) val flow = repository.getUserDetails(id = 1) val result = flow.single() result.isFailure.assertTrue() }

Slide 46

Slide 46 text

1. Flow emits Success 2. Flow emits Error 3. Retries with delay (Advanced)

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.catch { emit(Result.failure(it)) } } Coroutines-Library fun retry( retries: Long, block: suspend (Throwable) .>Boolean ): Flow

Slide 50

Slide 50 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 51

Slide 51 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 52

Slide 52 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 53

Slide 53 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(users)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 54

Slide 54 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 55

Slide 55 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 56

Slide 56 text

1. All Retries fail with Error 2. Retry succeeds

Slide 57

Slide 57 text

1. Launch a coroutine with a test scope and test dispatcher. 2. Advance virtual time forward.

Slide 58

Slide 58 text

@Test fun `should retry with error`() { } Coroutine-Test-Library fun runBlockingTest( block: suspend TestCoroutineScope.() .> Unit ) fun TestCoroutineDispatcher.runBlockingTest( block: suspend TestCoroutineScope.() .> Unit )

Slide 59

Slide 59 text

@Test fun `should retry with error`() = dispatcher.runBlockingTest { }

Slide 60

Slide 60 text

@Test fun `should retry with error`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> result.isFailure.assertTrue() } }

Slide 61

Slide 61 text

@Test fun `should retry with error`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> result.isFailure.assertTrue() } }

Slide 62

Slide 62 text

@Test fun `should retry with error`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> result.isFailure.assertTrue() } }

Slide 63

Slide 63 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 64

Slide 64 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 65

Slide 65 text

fun userDetails(id: Int): Flow { return flow { val users = userService.userDetails(id) emit(Result.success(users)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 66

Slide 66 text

fun userDetails(id: Int): Flow { return flow { val users = userService.userDetails(id) emit(Result.success(users)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Coroutine-Test-Library fun runBlockingTest { dispatcher.advanceUntilIdle() }

Slide 67

Slide 67 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 68

Slide 68 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 69

Slide 69 text

@Test fun `should retry with error`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> result.isFailure.assertTrue() } }

Slide 70

Slide 70 text

1. All Retries fail with Error 2. Retry succeeds

Slide 71

Slide 71 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 72

Slide 72 text

1. Pause Dispatcher 2. Advance virtual time forward by certain milliseconds.

Slide 73

Slide 73 text

@Test fun `should retry with success`() = dispatcher.runBlockingTest { pauseDispatcher { } } Coroutine-Test-Library pausedDispatcher - Do not start coroutines eagerly

Slide 74

Slide 74 text

@Test fun `should retry with success`() = dispatcher.runBlockingTest { var throwError = true val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } }

Slide 75

Slide 75 text

@Test fun `should retry with success`() = dispatcher.runBlockingTest { var throwError = true val apiService = mock { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } } }

Slide 76

Slide 76 text

@Test fun `should retry with success`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }

Slide 77

Slide 77 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 78

Slide 78 text

@Test fun `should retry with success`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }

Slide 79

Slide 79 text

@Test fun `should retry with success`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }

Slide 80

Slide 80 text

@Test fun `should retry with success`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }

Slide 81

Slide 81 text

fun userDetails(id: Int): Flow { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t -> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }

Slide 82

Slide 82 text

@Test fun `should retry with success`() = dispatcher.runBlockingTest { val apiService = mock { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }

Slide 83

Slide 83 text

1. Flow emits Success 2. Flow emits Error 3. Retries with delay

Slide 84

Slide 84 text

ViewModel Repository

Slide 85

Slide 85 text

class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) : ViewModel { viewModelScope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> stateManager.dispatchState(result) } } }

Slide 86

Slide 86 text

1. Dispatchers.setMain() 2. Dispatchers.resetMain()

Slide 87

Slide 87 text

class CoroutineTestRule( val dispatcher= TestCoroutineDispatcher() ) : TestWatcher() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(dispatcher) } }

Slide 88

Slide 88 text

class CoroutineTestRule( val dispatcher= TestCoroutineDispatcher() ) : TestWatcher() { override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() dispatcher.cleanupTestCoroutines() } }

Slide 89

Slide 89 text

@get:Rule val rule = CoroutineTestRule() val repository = mock() val stateManager = mock() val viewModel = UserDetailsViewModel(repository, stateManager)

Slide 90

Slide 90 text

@Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { }

Slide 91

Slide 91 text

class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) : ViewModel { viewModelScope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result -> stateManager.dispatchState(result) } } }

Slide 92

Slide 92 text

Flow Channel

Slide 93

Slide 93 text

@Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) }

Slide 94

Slide 94 text

@Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) val channel = Channel() val flow = channel.consumeAsFlow() whenever(repository.getUserDetails(id = 1)) doReturn flow } Flow Channel Convert to Flow

Slide 95

Slide 95 text

@Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) val channel = Channel() val flow = channel.consumeAsFlow() whenever(repository.getUserDetails(id = 1)) doReturn flow launch { channel.send(result) } } Flow Channel Send Producer to send values

Slide 96

Slide 96 text

@Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) val channel = Channel() val flow = channel.consumeAsFlow() whenever(repository.getUserDetails(id = 1)) doReturn flow launch { channel.send(result) } userDetailsViewModel.getUserDetails() verify(stateManager).dispatch(result) }

Slide 97

Slide 97 text

ViewModel Repository

Slide 98

Slide 98 text

@Test fun `should get users details on success`() = runBlocking { ... flow.collect { } } RxJava Observable.test()

Slide 99

Slide 99 text

Flow Channel API fun expectItem(): T fun expectNoMoreEvents() fun expectComplete() fun expectError(): Throwable

Slide 100

Slide 100 text

@Test fun `should get users details on success`() = runBlocking { ... flow.test { expectItem() assertEquals userDetails expectComplete() } }

Slide 101

Slide 101 text

● Channels & Flow in Practice https://speakerdeck.com/heyitsmohit/channels-and-flows-in-practice ● Dissecting Coroutines https://speakerdeck.com/heyitsmohit/dissecting-coroutines ● Koltin Channels Under the Hood https://medium.com/@heyitsmohit/kotlin-coroutine-channels-under-the-hood-part-1

Slide 102

Slide 102 text

No content