Android Unit Test
Bui Huu Phuoc
HCMC - Vietnam, 24/07/2020
1
Table of contents
- What’s Android testing?
- Fundamentals of Testing
- Unit tests
- Write unit tests for Clean Architecture
- Test coverage
- References
2
What’s Android testing?
- It’s a part of the app development process.
- Verify your app's correctness, functional behavior, and usability
before you release it publicly.
- Rapid feedback on failures.
- Early failure detection in the development cycle.
- Safer code refactoring, letting you optimize code without worrying about regressions.
- Stable development velocity, helping you minimize technical debt.
3
Fundamentals of Testing
- Organize your code for testing
- Configure your test environment
- Write your tests
4
Fundamentals of Testing →
Organize your code for testing
- Create and test code iteratively
5
Fundamentals of Testing →
Organize your code for testing
- View your app as a series of modules
6
Fundamentals of Testing →
Configure your test environment
- Organize test directories based on execution environment
- androidTest: Directory should contain the tests that run on real or virtual devices.
- Integration tests
- End-to-end tests
- Other tests where the JVM alone cannot validate
- test: Should contain the tests that run on your local machine.
- Unit tests
7
Fundamentals of Testing →
Configure your test environment
- Consider tradeoffs of running tests on different types of devices
- Real device
- Virtual device (such as the emulator in Android Studio)
- Simulated device (such as Robolectric)
8
Fundamentals of Testing →
Configure your test environment
- Consider whether to use test doubles
Real objects or Test doubles
Test doubles:
- Mock object
- Fake object
- Dummy object
- Test stub
- Test spy
9
Fundamentals of Testing →
Write your tests
- Small tests → unit tests
- Medium tests → integration tests
- Large tests → end-to-end tests
10% large
20% medium
70% small
10
Unit tests
- What’s unit tests?
- Types of unit tests:
- Local tests
- Instrumented tests
- Libraries support
11
Unit tests →
What’s unit tests?
- Unit tests are the fundamental tests
- Easily verify that the logic of individual units
is correct
- Running UTs after every build helps you to
quickly catch and fix.
12
Unit tests →
Types of unit tests
Local tests:
- Run on the local machine only.
- Compiled to run locally on JVM to minimize execution time.
- Depend on your own dependencies → using mock objects to emulate
the dependencies' behavior.
13
Unit tests →
Types of unit tests
Instrumented tests:
- Run on an Android device or emulator.
- Access to instrumentation information (such as the Context)
- Run unit tests that have complex Android dependencies that require a
more robust environment, such as Robolectric.
14
Unit tests →
Libraries support
- JUnit4 or JUnit5
- Mockito (or its Kotlin version mockito-kotlin)
- Mockk
- Robolectric
- AndroidJUnit4
15
Write unit tests for Clean Architecture
- Domain
- Use cases
- DI module
- Data
- Mapper
- Repository
- Remote
- Local
- DI module
- Presentation
- Mapper
- View Model
- DI Module
16
Write unit tests for Clean Architecture
- Domain
- Use cases
- DI module
17
Write unit tests for Clean Architecture → Domain →
Use cases
18
class GetTopUsersUseCase(
private val homeRepository: HomeRepository
) : UseCase<UseCaseParams.Empty, List<UserEntity>>() {
override suspend fun executeInternal(
params: UseCaseParams.Empty
): Either<Failure, List<UserEntity>> {
return homeRepository.getTopUsers()
}
}
Write unit tests for Clean Architecture → Domain →
Use cases Test success
19
@RunWith(JUnit4::class)
class GetTopUsersUseCaseTest {
private val repository = mockk<HomeRepository>()
private val useCase = GetTopUsersUseCase(repository)
@Test
fun executeInternal_success() = runBlocking {
val expected = provideUserEntityList()
coEvery { repository.getTopUsers() } returns Either.Success(expected)
useCase.execute(UseCaseParams.Empty).either(
failAction = { assertTrue(false) },
successAction = { actual -> assertEquals(expected, actual) }
)
}
}
Write unit tests for Clean Architecture → Domain →
Use cases Test error
20
@RunWith(JUnit4::class)
class GetTopUsersUseCaseTest {
private val repository = mockk<HomeRepository>()
private val useCase = GetTopUsersUseCase(repository)
@Test
fun executeInternal_error() = runBlocking {
val expected = provideFailEither()
coEvery { repository.getTopUsers() } returns expected
useCase.execute(UseCaseParams.Empty).either(
failAction = { actual -> assertEquals(actual, expected) },
successAction = { assertTrue(false) }
)
}
}
Write unit tests for Clean Architecture → Domain →
DI modules
21
val createDomainModule = module {
factory { GetTopUsersUseCase(homeRepository = get()) }
factory { GetUserDetailUseCase(detailRepository = get()) }
}
Write unit tests for Clean Architecture → Domain →
DI modules Test
22
@Category(CheckModuleTest::class)
class DomainModuleTest : AutoCloseKoinTest() {
@Test
fun checkModules() = checkModules {
val mockModule = module {
factory { mockk<HomeRepository>() }
factory { mockk<UserDetailRepository>() }
}
modules(createDomainModule, mockModule)
}
}
Write unit tests for Clean Architecture
- Data
- Mapper
- Repository
- Remote
- Local
- DI module
23
Write unit tests for Clean Architecture → Data→
Mapper
24
class UserRemoteEntityMapper : Mapper<UserResponse, UserEntity>() {
override fun map(input: UserResponse): UserEntity = UserEntity(
id = input.id.defaultEmpty(),
login = input.login.defaultEmpty(),
avatarUrl = input.avatarUrl.defaultEmpty(),
name = input.name.defaultEmpty(),
company = input.company.defaultEmpty(),
blog = input.blog.defaultEmpty(),
lastRefreshed = Date()
)
}
Write unit tests for Clean Architecture → Data→
Mapper Test
25
class UserRemoteEntityMapperTest {
private val userRemoteEntityMapper = UserRemoteEntityMapper()
@Test
fun map() {
val userResponse = provideUserResponse()
val actual = userRemoteEntityMapper.map(userResponse)
val expected = provideUserEntity().copy(lastRefreshed = actual.lastRefreshed)
assertEquals(expected, actual)
}
}
Write unit tests for Clean Architecture → Data→
Repository
26
class HomeRepositoryImpl(
private val userDataSource: UserDataSource,
private val userDao: UserDao,
private val remoteExceptionInterceptor: RemoteExceptionInterceptor,
private val userLocalEntityMapper: UserLocalEntityMapper,
private val userResponseLocalMapper: UserResponseLocalMapper
) : HomeRepository {
override suspend fun getTopUsers(): Either<Failure, List<UserEntity>> =
Either.runSuspendWithCatchError(errorInterceptors = listOf(remoteExceptionInterceptor)) {
val dbResult = userLocalEntityMapper.mapList(userDao.getTopUsers())
if (dbResult.isNullOrEmpty()) {
val response = userDataSource.fetchTopUsersAsync()
val userDBOs = userResponseLocalMapper.mapList(response.items)
userDao.insertUses(userDBOs)
val dbAfterInsert = userLocalEntityMapper.mapList(userDao.getTopUsers())
return@runSuspendWithCatchError Either.Success(dbAfterInsert)
} else {
return@runSuspendWithCatchError Either.Success(dbResult)
}
}
}
Write unit tests for Clean Architecture → Data→
Repository Mocking
27
class HomeRepositoryImplTest {
private val userDataSource: UserDataSource = mockk()
private val userDao: UserDao = mockk()
private val remoteInterceptor: RemoteExceptionInterceptor = mockk()
private val userLocalEntityMapper: UserLocalEntityMapper = mockk()
private val userResponseLocalMapper: UserResponseLocalMapper = mockk()
private val detailRepositoryImpl = HomeRepositoryImpl(
userDataSource = userDataSource,
userDao = userDao,
remoteExceptionInterceptor = remoteInterceptor,
userLocalEntityMapper = userLocalEntityMapper,
userResponseLocalMapper = userResponseLocalMapper
)
//...
}
Write unit tests for Clean Architecture → Data→
Repository Test success from remote
28
class HomeRepositoryImplTest {
// ...
@Test
fun getTopUsers_success_fromRemote() = runBlocking {
val userResponseList = provideUserResponseList()
val userDBOList = provideUserDBOList()
val expected = provideUserEntityList()
val userEntity = provideUserEntity()
coEvery { userDao.getTopUsers() } returnsMany listOf(listOf(), userDBOList)
coEvery { userDataSource.fetchTopUsersAsync() } returns userResponseList
coEvery { userDao.insertUses(userDBOList) } returns Unit
every { userLocalEntityMapper.mapList(listOf()) } returns listOf()
every { userResponseLocalMapper.mapList(userResponseList.items) } returns userDBOList
every { userLocalEntityMapper.mapList(userDBOList) } returns expected
detailRepositoryImpl.getTopUsers().either(
failAction = { assertTrue(false) },
successAction = { actual ->
assertEquals(expected, actual)
}
)
}
}
Write unit tests for Clean Architecture → Data→
Repository Test error
29
class HomeRepositoryImplTest {
@Test
fun getTopUsers_error() = runBlocking {
val exception = provideException()
coEvery { userDao.getTopUsers() } returns listOf()
coEvery { userDataSource.fetchTopUsersAsync() } throws exception
every { userLocalEntityMapper.mapList(listOf()) } returns listOf()
every {
remoteInterceptor.handleException(exception)
} returns Failure.UnCatchError(exception)
detailRepositoryImpl.getTopUsers().either(
failAction = { assertTrue(true) },
successAction = { assertTrue(false) }
)
}
}
Write unit tests for Clean Architecture
- Data
- Mapper
- Repository implemented
- Remote
- Local
- DI module
30
Write unit tests for Clean Architecture → Data→ Remote
UserDataSource
31
class UserDataSource(private val userService: UserService) {
suspend fun fetchTopUsersAsync() = userService.fetchTopUsersAsync()
}
Write unit tests for Clean Architecture → Data→ Remote
UserDataSource Test
32
class UserDataSourceTest {
private val userService: UserService = mockk()
private val userDataSource = UserDataSource(userService)
@Test
fun fetchTopUsersAsync() = runBlocking {
val expected = provideUserResponseList()
coEvery { userService.fetchTopUsersAsync() } returns expected
val actual = userDataSource.fetchTopUsersAsync()
Assert.assertEquals(expected, actual)
}
}
Write unit tests for Clean Architecture → Data→ Remote
UserService
33
interface UserService {
@GET("search/users")
suspend fun fetchTopUsersAsync(
@Query("q") query: String = "PhilippeB",
@Query("sort") sort: String = "followers"
): ApiResult<UserResponse>
}
Write unit tests for Clean Architecture → Data→ Remote
UserService Test
34
open class UserServiceTest() {
private lateinit var mockServer: MockWebServer
protected val userDataSource: UserDataSource by inject()
@Before
fun setup() {
mockServer = MockWebServer()
mockServer.start()
startKoin { modules(listOf(createRemoteModule(mockServer.url("/").toString()))) }
}
@After
fun tearDown() {
mockServer.shutdown()
stopKoin()
}
}
Write unit tests for Clean Architecture → Data→ Remote
UserService Test
35
class UserServiceTest() : AutoCloseKoinTest() {
@Test
fun fetchTopUsersAsync_success() {
mockServer.enqueue(
MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(getJson("search_users.json"))
)
runBlocking {
val users = userDataSource.fetchTopUsersAsync()
assertEquals(1, users.items.size)
assertEquals("6847959", users.items.first().id)
assertEquals("PhilippeBoisney", users.items.first().login)
assertEquals(
"https://avatars0.githubusercontent.com/u/6847959?v=4",
users.items.first().avatarUrl
)
}
}
}
Write unit tests for Clean Architecture
- Data
- Mapper
- Repository
- Remote
- Local
- DI module
36
Write unit tests for Clean Architecture → Data→ Local
Room DAO
37
@Dao
interface UserDao {
@Insert
suspend fun insertUses(userDBOS: List<UserDBO>)
@Query("SELECT * FROM UserDBO ORDER BY login ASC LIMIT 30")
suspend fun getTopUsers(): List<UserDBO>
}
Write unit tests for Clean Architecture → Data→ Local
Room DAO Test
38
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class UserDaoTest {
private lateinit var userDao: UserDao
private lateinit var database: MulAppDatabase
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
@Before
fun setup() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
database = Room.inMemoryDatabaseBuilder(context, MulAppDatabase::class.java)
.setTransactionExecutor(testDispatcher.asExecutor())
.setQueryExecutor(testDispatcher.asExecutor())
.build()
userDao = database.userDao()
}
@After
@Throws(IOException::class)
fun clear() {
database.close()
}
}
Write unit tests for Clean Architecture → Data→ Local
Room DAO Test
39
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class UserDaoTest {
// ...
@Test
fun test_insertUsers() = testScope.runBlockingTest {
val expected = provideUserDBOList(10)
userDao.insertUses(expected)
val actual = userDao.getTopUsers()
Assert.assertEquals(expected, actual)
}
}
Write unit tests for Clean Architecture
- Presentation
- Mapper
- View Model
- DI module
40
Write unit tests for Clean Architecture → Presentation →
ViewModel
41
class HomeViewModel(
private val getTopUsersUseCase: GetTopUsersUseCase,
private val appDispatchers: AppDispatchers
) : BaseViewModel() {
val usersLiveData: MutableLiveData<List<UserEntity>> = MutableLiveData()
val isLoading: MutableLiveData<Boolean> = MutableLiveData()
fun loadUsers() = viewModelScope.launch(appDispatchers.main) {
isLoading.value = true
val getUserResult = getTopUsersUseCase.execute(UseCaseParams.Empty)
isLoading.value = false
getUserResult.either(
failAction = {
usersLiveData.value = null
},
successAction = { userEntities ->
usersLiveData.value = userEntities
}
)
}
}
Write unit tests for Clean Architecture → Presentation →
ViewModel Mocking
42
@ExperimentalCoroutinesApi
class HomViewModelTest {
@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val getTopUsersUseCase = mockk<GetTopUsersUseCase>()
private val appDispatchers = AppDispatchers(TestCoroutineDispatcher(), TestCoroutineDispatcher())
private val homeViewModel = HomeViewModel(getTopUsersUseCase, appDispatchers)
// ...
}
Write unit tests for Clean Architecture → Presentation →
ViewModel Test
43
@ExperimentalCoroutinesApi
class HomeViewModelTest {
// ...
@Test
fun loadUsers_success() {
val expectedUsers = provideUserEntityList()
val observerUsers: Observer<List<UserEntity>> = mockk(relaxed = true)
val observerLoading: Observer<Boolean> = mockk(relaxed = true)
coEvery { getTopUsersUseCase.execute(UseCaseParams.Empty) } returns Either.Success(expectedUsers)
homeViewModel.usersLiveData.observeForever(observerUsers)
homeViewModel.isLoading.observeForever(observerLoading)
homeViewModel.loadUsers()
verify {
observerUsers.onChanged(expectedUsers)
}
verifySequence {
observerLoading.onChanged(true)
observerLoading.onChanged(false)
}
confirmVerified(observerUsers)
}
}
Write unit tests for Clean Architecture → Presentation → ViewModel Test →
Relaxed
44
class Divider() {
fun divide(p1: Int, p2: Int): Float {
return p1.toFloat() / p2
}
}
class Calculator(val div: Divider) {
fun executeDivide(p1: Int, p2: Int): Float {
return div.divide(p1, p2)
}
}
@Test
fun divTest() {
val mockDiv = mockk<Divider>(relaxed = true)
val cal = Calculator(div = mockDiv)
cal.executeDivide(1, 2)
verify { mockDiv.divide(1, 2) }
}
--------------------------------------------------------
@Test
fun divTest() {
val mockDiv = mockk<Divider>(relaxed = false)
every { mockDiv.divide(1, 2) } returns 0.5f
val cal = Calculator(div = mockDiv)
cal.executeDivide(1, 2)
verify { mockDiv.divide(1, 2) }
}
Test coverage
- Android Studio
- Jacoco
45
Test coverage →
Run code coverage in Android Studio
46
Navigate to the src/test/java folder
→ Right click
→ Select Run ‘Tests in ‘com’’ with Coverage
Test coverage →
Jacoco task
task fullCoverageReport(type: JacocoReport) {
dependsOn 'createDebugCoverageReport'
dependsOn 'testDebugUnitTest'
reports {
xml.enabled = true
html.enabled = true
}
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
'**/*Test*.*', 'android/**/*.*']
def debugTree = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
def mainSrc = "${project.projectDir}/src/main/java"
sourceDirectories = files([mainSrc])
classDirectories = files([debugTree])
executionData = fileTree(dir: "$buildDir", includes: [
"jacoco/testDebugUnitTest.exec",
"outputs/code-coverage/connected/*coverage.ec"
])
}
47
Test coverage →
Run a Jacoco task
48
Using
./gradlew fullCoverageReport OR
References
1. Test apps on Android
2. A guide to test pyramid in Android — Part 1
3. GithubBrowserSample on GitHub
4. MVP Clean Architecture in Tiendeo app
5. Unit test pull request from NHN-base source
49
50

Android Unit Test

  • 1.
    Android Unit Test BuiHuu Phuoc HCMC - Vietnam, 24/07/2020 1
  • 2.
    Table of contents -What’s Android testing? - Fundamentals of Testing - Unit tests - Write unit tests for Clean Architecture - Test coverage - References 2
  • 3.
    What’s Android testing? -It’s a part of the app development process. - Verify your app's correctness, functional behavior, and usability before you release it publicly. - Rapid feedback on failures. - Early failure detection in the development cycle. - Safer code refactoring, letting you optimize code without worrying about regressions. - Stable development velocity, helping you minimize technical debt. 3
  • 4.
    Fundamentals of Testing -Organize your code for testing - Configure your test environment - Write your tests 4
  • 5.
    Fundamentals of Testing→ Organize your code for testing - Create and test code iteratively 5
  • 6.
    Fundamentals of Testing→ Organize your code for testing - View your app as a series of modules 6
  • 7.
    Fundamentals of Testing→ Configure your test environment - Organize test directories based on execution environment - androidTest: Directory should contain the tests that run on real or virtual devices. - Integration tests - End-to-end tests - Other tests where the JVM alone cannot validate - test: Should contain the tests that run on your local machine. - Unit tests 7
  • 8.
    Fundamentals of Testing→ Configure your test environment - Consider tradeoffs of running tests on different types of devices - Real device - Virtual device (such as the emulator in Android Studio) - Simulated device (such as Robolectric) 8
  • 9.
    Fundamentals of Testing→ Configure your test environment - Consider whether to use test doubles Real objects or Test doubles Test doubles: - Mock object - Fake object - Dummy object - Test stub - Test spy 9
  • 10.
    Fundamentals of Testing→ Write your tests - Small tests → unit tests - Medium tests → integration tests - Large tests → end-to-end tests 10% large 20% medium 70% small 10
  • 11.
    Unit tests - What’sunit tests? - Types of unit tests: - Local tests - Instrumented tests - Libraries support 11
  • 12.
    Unit tests → What’sunit tests? - Unit tests are the fundamental tests - Easily verify that the logic of individual units is correct - Running UTs after every build helps you to quickly catch and fix. 12
  • 13.
    Unit tests → Typesof unit tests Local tests: - Run on the local machine only. - Compiled to run locally on JVM to minimize execution time. - Depend on your own dependencies → using mock objects to emulate the dependencies' behavior. 13
  • 14.
    Unit tests → Typesof unit tests Instrumented tests: - Run on an Android device or emulator. - Access to instrumentation information (such as the Context) - Run unit tests that have complex Android dependencies that require a more robust environment, such as Robolectric. 14
  • 15.
    Unit tests → Librariessupport - JUnit4 or JUnit5 - Mockito (or its Kotlin version mockito-kotlin) - Mockk - Robolectric - AndroidJUnit4 15
  • 16.
    Write unit testsfor Clean Architecture - Domain - Use cases - DI module - Data - Mapper - Repository - Remote - Local - DI module - Presentation - Mapper - View Model - DI Module 16
  • 17.
    Write unit testsfor Clean Architecture - Domain - Use cases - DI module 17
  • 18.
    Write unit testsfor Clean Architecture → Domain → Use cases 18 class GetTopUsersUseCase( private val homeRepository: HomeRepository ) : UseCase<UseCaseParams.Empty, List<UserEntity>>() { override suspend fun executeInternal( params: UseCaseParams.Empty ): Either<Failure, List<UserEntity>> { return homeRepository.getTopUsers() } }
  • 19.
    Write unit testsfor Clean Architecture → Domain → Use cases Test success 19 @RunWith(JUnit4::class) class GetTopUsersUseCaseTest { private val repository = mockk<HomeRepository>() private val useCase = GetTopUsersUseCase(repository) @Test fun executeInternal_success() = runBlocking { val expected = provideUserEntityList() coEvery { repository.getTopUsers() } returns Either.Success(expected) useCase.execute(UseCaseParams.Empty).either( failAction = { assertTrue(false) }, successAction = { actual -> assertEquals(expected, actual) } ) } }
  • 20.
    Write unit testsfor Clean Architecture → Domain → Use cases Test error 20 @RunWith(JUnit4::class) class GetTopUsersUseCaseTest { private val repository = mockk<HomeRepository>() private val useCase = GetTopUsersUseCase(repository) @Test fun executeInternal_error() = runBlocking { val expected = provideFailEither() coEvery { repository.getTopUsers() } returns expected useCase.execute(UseCaseParams.Empty).either( failAction = { actual -> assertEquals(actual, expected) }, successAction = { assertTrue(false) } ) } }
  • 21.
    Write unit testsfor Clean Architecture → Domain → DI modules 21 val createDomainModule = module { factory { GetTopUsersUseCase(homeRepository = get()) } factory { GetUserDetailUseCase(detailRepository = get()) } }
  • 22.
    Write unit testsfor Clean Architecture → Domain → DI modules Test 22 @Category(CheckModuleTest::class) class DomainModuleTest : AutoCloseKoinTest() { @Test fun checkModules() = checkModules { val mockModule = module { factory { mockk<HomeRepository>() } factory { mockk<UserDetailRepository>() } } modules(createDomainModule, mockModule) } }
  • 23.
    Write unit testsfor Clean Architecture - Data - Mapper - Repository - Remote - Local - DI module 23
  • 24.
    Write unit testsfor Clean Architecture → Data→ Mapper 24 class UserRemoteEntityMapper : Mapper<UserResponse, UserEntity>() { override fun map(input: UserResponse): UserEntity = UserEntity( id = input.id.defaultEmpty(), login = input.login.defaultEmpty(), avatarUrl = input.avatarUrl.defaultEmpty(), name = input.name.defaultEmpty(), company = input.company.defaultEmpty(), blog = input.blog.defaultEmpty(), lastRefreshed = Date() ) }
  • 25.
    Write unit testsfor Clean Architecture → Data→ Mapper Test 25 class UserRemoteEntityMapperTest { private val userRemoteEntityMapper = UserRemoteEntityMapper() @Test fun map() { val userResponse = provideUserResponse() val actual = userRemoteEntityMapper.map(userResponse) val expected = provideUserEntity().copy(lastRefreshed = actual.lastRefreshed) assertEquals(expected, actual) } }
  • 26.
    Write unit testsfor Clean Architecture → Data→ Repository 26 class HomeRepositoryImpl( private val userDataSource: UserDataSource, private val userDao: UserDao, private val remoteExceptionInterceptor: RemoteExceptionInterceptor, private val userLocalEntityMapper: UserLocalEntityMapper, private val userResponseLocalMapper: UserResponseLocalMapper ) : HomeRepository { override suspend fun getTopUsers(): Either<Failure, List<UserEntity>> = Either.runSuspendWithCatchError(errorInterceptors = listOf(remoteExceptionInterceptor)) { val dbResult = userLocalEntityMapper.mapList(userDao.getTopUsers()) if (dbResult.isNullOrEmpty()) { val response = userDataSource.fetchTopUsersAsync() val userDBOs = userResponseLocalMapper.mapList(response.items) userDao.insertUses(userDBOs) val dbAfterInsert = userLocalEntityMapper.mapList(userDao.getTopUsers()) return@runSuspendWithCatchError Either.Success(dbAfterInsert) } else { return@runSuspendWithCatchError Either.Success(dbResult) } } }
  • 27.
    Write unit testsfor Clean Architecture → Data→ Repository Mocking 27 class HomeRepositoryImplTest { private val userDataSource: UserDataSource = mockk() private val userDao: UserDao = mockk() private val remoteInterceptor: RemoteExceptionInterceptor = mockk() private val userLocalEntityMapper: UserLocalEntityMapper = mockk() private val userResponseLocalMapper: UserResponseLocalMapper = mockk() private val detailRepositoryImpl = HomeRepositoryImpl( userDataSource = userDataSource, userDao = userDao, remoteExceptionInterceptor = remoteInterceptor, userLocalEntityMapper = userLocalEntityMapper, userResponseLocalMapper = userResponseLocalMapper ) //... }
  • 28.
    Write unit testsfor Clean Architecture → Data→ Repository Test success from remote 28 class HomeRepositoryImplTest { // ... @Test fun getTopUsers_success_fromRemote() = runBlocking { val userResponseList = provideUserResponseList() val userDBOList = provideUserDBOList() val expected = provideUserEntityList() val userEntity = provideUserEntity() coEvery { userDao.getTopUsers() } returnsMany listOf(listOf(), userDBOList) coEvery { userDataSource.fetchTopUsersAsync() } returns userResponseList coEvery { userDao.insertUses(userDBOList) } returns Unit every { userLocalEntityMapper.mapList(listOf()) } returns listOf() every { userResponseLocalMapper.mapList(userResponseList.items) } returns userDBOList every { userLocalEntityMapper.mapList(userDBOList) } returns expected detailRepositoryImpl.getTopUsers().either( failAction = { assertTrue(false) }, successAction = { actual -> assertEquals(expected, actual) } ) } }
  • 29.
    Write unit testsfor Clean Architecture → Data→ Repository Test error 29 class HomeRepositoryImplTest { @Test fun getTopUsers_error() = runBlocking { val exception = provideException() coEvery { userDao.getTopUsers() } returns listOf() coEvery { userDataSource.fetchTopUsersAsync() } throws exception every { userLocalEntityMapper.mapList(listOf()) } returns listOf() every { remoteInterceptor.handleException(exception) } returns Failure.UnCatchError(exception) detailRepositoryImpl.getTopUsers().either( failAction = { assertTrue(true) }, successAction = { assertTrue(false) } ) } }
  • 30.
    Write unit testsfor Clean Architecture - Data - Mapper - Repository implemented - Remote - Local - DI module 30
  • 31.
    Write unit testsfor Clean Architecture → Data→ Remote UserDataSource 31 class UserDataSource(private val userService: UserService) { suspend fun fetchTopUsersAsync() = userService.fetchTopUsersAsync() }
  • 32.
    Write unit testsfor Clean Architecture → Data→ Remote UserDataSource Test 32 class UserDataSourceTest { private val userService: UserService = mockk() private val userDataSource = UserDataSource(userService) @Test fun fetchTopUsersAsync() = runBlocking { val expected = provideUserResponseList() coEvery { userService.fetchTopUsersAsync() } returns expected val actual = userDataSource.fetchTopUsersAsync() Assert.assertEquals(expected, actual) } }
  • 33.
    Write unit testsfor Clean Architecture → Data→ Remote UserService 33 interface UserService { @GET("search/users") suspend fun fetchTopUsersAsync( @Query("q") query: String = "PhilippeB", @Query("sort") sort: String = "followers" ): ApiResult<UserResponse> }
  • 34.
    Write unit testsfor Clean Architecture → Data→ Remote UserService Test 34 open class UserServiceTest() { private lateinit var mockServer: MockWebServer protected val userDataSource: UserDataSource by inject() @Before fun setup() { mockServer = MockWebServer() mockServer.start() startKoin { modules(listOf(createRemoteModule(mockServer.url("/").toString()))) } } @After fun tearDown() { mockServer.shutdown() stopKoin() } }
  • 35.
    Write unit testsfor Clean Architecture → Data→ Remote UserService Test 35 class UserServiceTest() : AutoCloseKoinTest() { @Test fun fetchTopUsersAsync_success() { mockServer.enqueue( MockResponse() .setResponseCode(HttpURLConnection.HTTP_OK) .setBody(getJson("search_users.json")) ) runBlocking { val users = userDataSource.fetchTopUsersAsync() assertEquals(1, users.items.size) assertEquals("6847959", users.items.first().id) assertEquals("PhilippeBoisney", users.items.first().login) assertEquals( "https://avatars0.githubusercontent.com/u/6847959?v=4", users.items.first().avatarUrl ) } } }
  • 36.
    Write unit testsfor Clean Architecture - Data - Mapper - Repository - Remote - Local - DI module 36
  • 37.
    Write unit testsfor Clean Architecture → Data→ Local Room DAO 37 @Dao interface UserDao { @Insert suspend fun insertUses(userDBOS: List<UserDBO>) @Query("SELECT * FROM UserDBO ORDER BY login ASC LIMIT 30") suspend fun getTopUsers(): List<UserDBO> }
  • 38.
    Write unit testsfor Clean Architecture → Data→ Local Room DAO Test 38 @RunWith(AndroidJUnit4::class) @ExperimentalCoroutinesApi class UserDaoTest { private lateinit var userDao: UserDao private lateinit var database: MulAppDatabase private val testDispatcher = TestCoroutineDispatcher() private val testScope = TestCoroutineScope(testDispatcher) @Before fun setup() { val context = InstrumentationRegistry.getInstrumentation().targetContext database = Room.inMemoryDatabaseBuilder(context, MulAppDatabase::class.java) .setTransactionExecutor(testDispatcher.asExecutor()) .setQueryExecutor(testDispatcher.asExecutor()) .build() userDao = database.userDao() } @After @Throws(IOException::class) fun clear() { database.close() } }
  • 39.
    Write unit testsfor Clean Architecture → Data→ Local Room DAO Test 39 @RunWith(AndroidJUnit4::class) @ExperimentalCoroutinesApi class UserDaoTest { // ... @Test fun test_insertUsers() = testScope.runBlockingTest { val expected = provideUserDBOList(10) userDao.insertUses(expected) val actual = userDao.getTopUsers() Assert.assertEquals(expected, actual) } }
  • 40.
    Write unit testsfor Clean Architecture - Presentation - Mapper - View Model - DI module 40
  • 41.
    Write unit testsfor Clean Architecture → Presentation → ViewModel 41 class HomeViewModel( private val getTopUsersUseCase: GetTopUsersUseCase, private val appDispatchers: AppDispatchers ) : BaseViewModel() { val usersLiveData: MutableLiveData<List<UserEntity>> = MutableLiveData() val isLoading: MutableLiveData<Boolean> = MutableLiveData() fun loadUsers() = viewModelScope.launch(appDispatchers.main) { isLoading.value = true val getUserResult = getTopUsersUseCase.execute(UseCaseParams.Empty) isLoading.value = false getUserResult.either( failAction = { usersLiveData.value = null }, successAction = { userEntities -> usersLiveData.value = userEntities } ) } }
  • 42.
    Write unit testsfor Clean Architecture → Presentation → ViewModel Mocking 42 @ExperimentalCoroutinesApi class HomViewModelTest { @Rule @JvmField val instantTaskExecutorRule = InstantTaskExecutorRule() private val getTopUsersUseCase = mockk<GetTopUsersUseCase>() private val appDispatchers = AppDispatchers(TestCoroutineDispatcher(), TestCoroutineDispatcher()) private val homeViewModel = HomeViewModel(getTopUsersUseCase, appDispatchers) // ... }
  • 43.
    Write unit testsfor Clean Architecture → Presentation → ViewModel Test 43 @ExperimentalCoroutinesApi class HomeViewModelTest { // ... @Test fun loadUsers_success() { val expectedUsers = provideUserEntityList() val observerUsers: Observer<List<UserEntity>> = mockk(relaxed = true) val observerLoading: Observer<Boolean> = mockk(relaxed = true) coEvery { getTopUsersUseCase.execute(UseCaseParams.Empty) } returns Either.Success(expectedUsers) homeViewModel.usersLiveData.observeForever(observerUsers) homeViewModel.isLoading.observeForever(observerLoading) homeViewModel.loadUsers() verify { observerUsers.onChanged(expectedUsers) } verifySequence { observerLoading.onChanged(true) observerLoading.onChanged(false) } confirmVerified(observerUsers) } }
  • 44.
    Write unit testsfor Clean Architecture → Presentation → ViewModel Test → Relaxed 44 class Divider() { fun divide(p1: Int, p2: Int): Float { return p1.toFloat() / p2 } } class Calculator(val div: Divider) { fun executeDivide(p1: Int, p2: Int): Float { return div.divide(p1, p2) } } @Test fun divTest() { val mockDiv = mockk<Divider>(relaxed = true) val cal = Calculator(div = mockDiv) cal.executeDivide(1, 2) verify { mockDiv.divide(1, 2) } } -------------------------------------------------------- @Test fun divTest() { val mockDiv = mockk<Divider>(relaxed = false) every { mockDiv.divide(1, 2) } returns 0.5f val cal = Calculator(div = mockDiv) cal.executeDivide(1, 2) verify { mockDiv.divide(1, 2) } }
  • 45.
    Test coverage - AndroidStudio - Jacoco 45
  • 46.
    Test coverage → Runcode coverage in Android Studio 46 Navigate to the src/test/java folder → Right click → Select Run ‘Tests in ‘com’’ with Coverage
  • 47.
    Test coverage → Jacocotask task fullCoverageReport(type: JacocoReport) { dependsOn 'createDebugCoverageReport' dependsOn 'testDebugUnitTest' reports { xml.enabled = true html.enabled = true } def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] def debugTree = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: fileFilter) def mainSrc = "${project.projectDir}/src/main/java" sourceDirectories = files([mainSrc]) classDirectories = files([debugTree]) executionData = fileTree(dir: "$buildDir", includes: [ "jacoco/testDebugUnitTest.exec", "outputs/code-coverage/connected/*coverage.ec" ]) } 47
  • 48.
    Test coverage → Runa Jacoco task 48 Using ./gradlew fullCoverageReport OR
  • 49.
    References 1. Test appson Android 2. A guide to test pyramid in Android — Part 1 3. GithubBrowserSample on GitHub 4. MVP Clean Architecture in Tiendeo app 5. Unit test pull request from NHN-base source 49
  • 50.

Editor's Notes

  • #44  @Test fun divTest() { val mockDiv = mockk<Div>(relaxed = true) // every { mockDiv.divide(1, 2) } returns 0.5f val cal = Calculator(div = mockDiv) cal.executeDivide(1, 2) verify { mockDiv.divide(1, 2) } } class Div() { fun divide(p1: Int, p2: Int) = p1.toFloat()/p2 } class Calculator(val div: Div) { fun executeDivide(p1: Int, p2: Int) = div.divide(p1, p2) }