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’s unit tests?
- Types of unit tests:
- Local tests
- Instrumented tests
- Libraries support
11
12. 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
13. 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
14. 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
15. Unit tests →
Libraries support
- JUnit4 or JUnit5
- Mockito (or its Kotlin version mockito-kotlin)
- Mockk
- Robolectric
- AndroidJUnit4
15
16. 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
17. Write unit tests for Clean Architecture
- Domain
- Use cases
- DI module
17
18. 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()
}
}
19. 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) }
)
}
}
20. 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) }
)
}
}
21. Write unit tests for Clean Architecture → Domain →
DI modules
21
val createDomainModule = module {
factory { GetTopUsersUseCase(homeRepository = get()) }
factory { GetUserDetailUseCase(detailRepository = get()) }
}
22. 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)
}
}
23. Write unit tests for Clean Architecture
- Data
- Mapper
- Repository
- Remote
- Local
- DI module
23
24. 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()
)
}
25. 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)
}
}
26. 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)
}
}
}
27. 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
)
//...
}
28. 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)
}
)
}
}
29. 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) }
)
}
}
30. Write unit tests for Clean Architecture
- Data
- Mapper
- Repository implemented
- Remote
- Local
- DI module
30
31. Write unit tests for Clean Architecture → Data→ Remote
UserDataSource
31
class UserDataSource(private val userService: UserService) {
suspend fun fetchTopUsersAsync() = userService.fetchTopUsersAsync()
}
32. 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)
}
}
34. 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()
}
}
35. 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
)
}
}
}
36. Write unit tests for Clean Architecture
- Data
- Mapper
- Repository
- Remote
- Local
- DI module
36
37. 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>
}
38. 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()
}
}
39. 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)
}
}
40. Write unit tests for Clean Architecture
- Presentation
- Mapper
- View Model
- DI module
40
41. 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
}
)
}
}
42. 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)
// ...
}
43. 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)
}
}
44. 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) }
}
46. 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
49. 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