android 项目单元测试,示例多模块Android项目,包括单元测试,dagger 2,测试覆盖率等...

625332134c6f4d4600884b99daebf603.png

MultiModuleGithubClient

Breaking the monolith to microservices is a well-known concept to make backend solutions extendable and maintainable in a scale, by bigger teams. Since mobile apps have become more complex, very often developed by teams of tens of software engineers this concept also grows in mobile platforms. There are many benefits from having apps split into modules/features/libraries:

features can be developed independently

project structure is cleaner

building process can be way faster (e.g., running unit tests on a module can be a matter of seconds, instead of minutes for the entire project)

great starting point for instant apps

This is the example project which resolves (or at least workarounds) the most common problems with the multi-module Android app.

Project structure

The project is a straightforward Github API client containing 3 screens (user search, repositories list, repository details). For the sake of simplicity each screen is a separate module:

app - application module containing main app screen (user search)

repositories - repositories list

repository - repository detail

base - module containing a code shared between all modules

625332134c6f4d4600884b99daebf603.png

Dependencies management

It is easy to get lost with dependencies management across different project modules (especially with libs versions). To make it easier, take a look at buildsystem/dependencies.gradle where everything is configured. Each module has separate configuration, with additional two for testing and annotation processing. Like some other patterns, this was originally introduced in Azimo Android application by @dbarwacz.

Dagger 2

Recently there is no recommended Dagger 2 configuration for multi-module Android project. Some software engineers recommend exposing Dagger modules from Android feature module and use them in Components maintained only in the main App module. This project implements the second way - each feature module has its Component and dependencies tree. All of them depends on Base Component (created in Base module). All have their scope and subcomponents. This approach was proposed by my colleague @dbarwacz and recently is heavily used in Azimo Android application.

Here are some highlights from it:

BaseComponent is used as a dependency in feature components (e.g. RepositoriesFeatureComponent, AppComponent...). It means that all dependencies that are used in needs to be publicly exposed in BaseComponent interface.

Local components, like SplashActivityComponent are subcomponents of feature component (SplashActivityComponents is a subcomponent of AppComponent).

Each module has its own Scope (e.g. RepositoryFeatureScope, AppScope). Effectively they define singletons - they live as long as components, which are maintained by classes: AppComponentWrapper, RepositoryFeatureComponentWrapper which are singletons... . To have better control on Scopes lifecycle, a good idea would be to add release() method to ComponentWrappers.

For the rest take a look at the code - should be self-explaining. As this is just self-invented setup (that works on production!), all kind of feedback is warmly welcomed.

625332134c6f4d4600884b99daebf603.png

Unit Testing

Project contains some example unit tests for presenter classes.

Gradle

To run all unit tests from all modules at once execute:

./gradlew testDebugUnitTest

In console you can see:

...

> Task :app:testDebugUnitTest

com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testNavigation_whenUserLoaded_thenShouldNavigateToRepositoriesList PASSED

com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testErrorHandling_whenErrorOccuredWhileLoadingUser_thenShouldShowValidationError PASSED

com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testValidation_whenUserNameIsInvalid_thenShouldShowValidationError PASSED

com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testValidation_whenUserNameValid_thenShouldLoadUser PASSED

com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testInit_shouldLogLaunchedScreenIntoAnalytics PASSED

> Task :features:base:testDebugUnitTest

com.frogermcs.multimodulegithubclient.base.ExampleUnitTest > addition_isCorrect PASSED

> Task :features:repositories:testDebugUnitTest

com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivityPresenterTest > testRepositories_whenRepositoriesAreLoaded_thenShouldBePresented PASSED

com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivityPresenterTest > testInit_shouldLoadRepositoriesForGivenUser PASSED

com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivityPresenterTest > testNavigation_whenRepositoryClicked_thenShouldLaunchRepositoryDetails PASSED

com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivityPresenterTest > testInit_shouldLogLaunchedScreenIntoAnalytics PASSED

> Task :features:repository:testDebugUnitTest

com.frogermcs.multimodulegithubclient.repository.RepositoryDetailsActivityPresenterTest > testUnit_shouldSetRepositoryDetails PASSED

com.frogermcs.multimodulegithubclient.repository.RepositoryDetailsActivityPresenterTest > testInit_shouldSetUserName PASSED

com.frogermcs.multimodulegithubclient.repository.RepositoryDetailsActivityPresenterTest > testInit_shouldLogLaunchedScreenIntoAnalytics PASSED

BUILD SUCCESSFUL in 55s

...

It can be useful especially when you run tests on your CI environment. To control logs take a look at file buildsystem/android_commons.gradle (it is included in all feature modules).

testOptions {

unitTests {

all {

testLogging {

events 'passed', 'skipped', 'failed'

}

}

}

}

In case you want to run unit test in one module, execute:

./gradlew clean app:testDebugUnitTest

or

/gradlew clean feature:repository:testDebugUnitTest

Android Studio

The repository contains shared configurations for running Unit Tests directly from Android Studio.

625332134c6f4d4600884b99daebf603.png

All tests at once

Recently it is not always possible to run all tests at once (see troubleshooting below).

All tests, module after module sequentially

Run App and modules tests. This configuration will run unit tests from all modules in separate tabs, one after another. To modify list of tests, click on Edit Configurations -> App and modules tests -> Before Launch.

625332134c6f4d4600884b99daebf603.png

Troubleshooting

Class not found: ... Empty test suite. There is a bug in Android Studio which prevents from launching all unit tests at once, before their code is generated (what happens after the first run of unit tests for every single module independently). For more take a look at Android Studio bug tracker: https://issuetracker.google.com/issues/111154138.

625332134c6f4d4600884b99daebf603.png

Test coverage for unit tests

The project contains additional configuration for Jacoco that enables coverage report for Unit Tests (initially Jacoco reports cover Android Instrumentation Tests).

To run tests coverage, execute:

./gradlew testDebugUnitTestCoverage

Coverage report can be found in app/build/reports/jacoco/testDebugUnitTestCoverage/html/index.html (there is also an .xml file in case you would like to integrate coverage report with CI/CD environment.

Implementation details

Setting up a coverage report for Android Project isn't still straightforward and can take a couple of hours/days of exploration. Example setup in this project could be a little bit easier and more elegant, but some solutions are coded explicitly for better clarity. Here are some highlights:

Each module should use Jacoco plugin apply plugin: 'jacoco' and config (defined in android_commons.gradle):

buildTypes {

debug {

testCoverageEnabled true

}

}

App module defines custom Jacoco task called testDebugUnitTestCoverage. Entire configuration can be found in buildsystem/jacoco.gradle. The code should be self-explaining.

Task testDebugUnitTestCoverage depends on testDebugUnitTest tasks (each module separately). Thanks to it all sources required for coverage report are available before gradle starts generating it (in /build/....

625332134c6f4d4600884b99daebf603.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值