Gradle Android - Test coverage on legacy test projects structure - android

I am migrating a quite big and complex legacy project to Gradle.
I have many libraries projects, where each project has its own test subprojects.
The project structure looks as follows:
- LibraryX
-- LibraryXTestsOne
-- LibraryXTestsTwo
- LibraryY
-- LibraryYTests
....
Each library is configured as follows:
The main sourceset contains all the library code
The androidTest sourceset is empty
Each test project is configured as follows:
It lists the parent library as a dependency
The main sourceset is empty
The androidTest sourceset contains all the tests code
With such a configuration I manage to run the test with :libraryTestsA:connectedAndroidTest, but I can't get the test coverage to work. I enable it through
buildTypes{
debug{ testCoverageEnabled true }
}
Apparently the classes of the parent library are not instrumented causing the code coverage to be zero.
Any suggestions?

It turns out that you just need to set testCoverageEnabled to true for all the projects/libraries that the test project depends on.
That will be enough to instrument your code.
You still need to create a custom jacocoReport task though, since the one provided by the android plugin looks up classes and sources in the project main sourceset, which are empty in a configuration like mine.

Related

JaCoCo with Gradle Kotlin multi-module Android project - what does isTestCoverageEnabled actually do?

I'm configuring an Android multi-module Gradle project that uses Kotlin for both the app AND the Gradle build files (gradle.build.kts).
I'm using Gradle 7.3.3.
First I add the Jacoco plugin to the module-level build.gradle.kts:
...
plugins {
...
jacoco
}
...
Then I click the icon in Android Studio to "sync project with gradle files."
Next, I find the debug build type and add this:
isTestCoverageEnabled = true
When I subsequently run ./gradlew testDebugUnitTest, a file is generated in the module at <MODULE>/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec.
If, however, I don't add the line isTestCoverageEnabled = true, or if I set isTestCoverageEnabled = false, a coverage results file is generated in the module at <MODULE>/build/jacoco/testDebugUnitTest.exec.
When I generate each .exec file's HTML report, the "Total" rows at the bottom have matching counts.
Are these two files equivalent, but just located in different directories depending on the value of isTestCoverageEnabled?
If so, it seems that, as long as jacoco is included in the plugins, coverage results are generated regardless of whether the isTestCoverageEnabled = true line is added. Does isTestCoverageEnabled do anything else besides change the output directory? What am I missing?
I have struggled with this issue for 3 years across multiple projects, but I have a solution (or rather someone who resolved my issues).
The Android-Root-Coverage-Plugin can be used to combine both jUnit and instrumented tests in a Java and/or Kotlin multi module project without any great need to configure anything else. It resolved my following issues:
Getting coverage in multi module project
Incorrect coverage in Kotlin
Combining both jUnit & Instrumented test coverage
It has been confirmed that the cause of my issues were due to a bug in the android gradle plugin and as of Aug, 2022 it has yet to be resolved.
Specific to your question, isTestCoverageEnabled is used to configure the embedded JaCoCo in the android gradle plugin, which as stated above, is known to be buggy. Using the JaCoCo plugin can be configured in the gradle file but does not utilise the embedded JaCoCo in AGP

Gradle/Android: includeBuild sets build variant to null

Had no luck posting through the gradle community, so I thought I would reach out here.
In my root settings.gradle file, I use includeBuild to include the projects of interest. This works and I see that the builds have been included. Each one of these android libraries may contain a dependency on another module in a different project in the monorepo. All of these libraries produce artifacts that are published, so normally I would just target the recently released artifact. I want to use includeBuild with dependency substitution when developing locally. This way, if I make a change in a dependency I can make changes in all dependents immediately without having to release an artifact first.
Let me show you an example of one that I have working:
I have directory called base-implementation, this directory contains two gradle projects: base-api and base-ui-api. Each of these projects contains one singular android library module api and ui-api respectively. To further illustrate, one drilldown from a package structure would go base-implementation → base-api → api where api may be defined as an artifact dependency in other android libraries in this monorepo. For example ui-api defines a dependency on api.
Given this structure, in my base-ui-api project, I define an includeBuild on base-api and substitute out the dependency with the android library api. That looks like this:
def apiDependency = "com.myapp.example:api"
if(isIntegrationBuild.toBoolean()) {
includeBuild('../base-api') {
dependencySubstitution {
substitute module(apiDependency) using project(':api')
}
}
}
The isIntegrationBuild is just a gradle property I have set up as a development flag to use dependency substitution. I can run the gradle task provided by android called androidDependencies which will list all resolved dependencies for a given build. I can verify here that the dependency is indeed subbed out in ui-api by confirming this line “:api (variant: debug)”
It is important to note at this point, every single android library in this mono repo has only two build variants of debug and release there are no inconsistencies here with build variants.
The problem I am running into is this. I have a library that defines a dependency on another library which then defines a dependency on api. When going to build this library, a random nullPointerException is thrown without any message. What I have noticed however, is this; When I run androidDependencies task on the library that it is dependent on (the library that contains a dependency on api), the subbed out api dependency comes back with this “:api (variant: null)” and I cannot for the life of me figure out why. All includeBuilds and dependency substitution definitions are exactly the same. yet one resolves with variant debug and the other does not. They have next to identical build.gradle files at both the project and module level as well. There is nothing special happening to explicitly define a default variant implementation and all variants match across all libraries.
If what I explained above is unclear, let me drive home the issue with the project structure visual that doesn’t work. There is sensitive information in these library names so for sake of censoring, I will replace their names with A and B. Where A depends on B and B depends on api. A and B also share the same root dir but this root dir is just a container. Not a gradle project.
A depends on B so in the project level settings.gradle of A I define an includeBuild with a dependency substitution for B. This looks like the following:
def BDependency = "com.example.b:b"
if (isIntegrationBuild.toBoolean()) {
includeBuild('../project-b') {
dependencySubstitution {
substitute module(BDependency) using project(':b')
}
}
}
This block runs and works, I can see and confirm that the artifact is swapped out with the included build.
Now, B defines a dependency on api so its project level settings.gradle looks almost identical to the first case I stated where ui-api depends on api the only difference is relative pathing for includeBuild. This looks like the following:
def apiDependency = "com.myapp.example:api"
if(isIntegrationBuild.toBoolean()) {
includeBuild('../../base-api') {
dependencySubstitution {
substitute module(apiDependency) using project(':api')
}
}
}
Why is that when I build ui-api it pulls in api of variant debug, but when I build B it pulls in api of variant null? Is there a something I am missing about nested includedBuilds?

How to exclude Dagger2 classes from test coverage

Is there any option to exclude Dagger2 classes from test coverage report in Android Studio
JaCoCo excludes
If you're using JaCoCo, for example using android instrumentation connected tests, you need to configure the excludes (or includes), which, according to the documentation is...
A list of class files to exclude from the report. May use wildcard characters (* and ?). When not specified nothing will be excluded.
Which means you need to match the generated dagger class names. The following rules cover virtually any class generated by dagger-compiler, without matching any of non-generated classes (unless you name your class the same as dagger does...):
excludes = [
'**/*_MembersInjector.class',
'**/Dagger*Component.class', // covers component implementations
'**/Dagger*Component$Builder.class', // covers component builders
'**/*Module_*Factory.class'
]
You can check your generated dagger classes in app/build/generated/source/apt directory after running a build, to see if there are any additional generated classes that you want to match with excludes.
This excludes array is a configuration property of jacoco plugin. Now, where to put this excludes array depends on whether you define your own tasks based on the jacoco plugin, or use a 'higher level plugin' that does this for you. For example using this plugin (you can see the plugin source to see where the excludes are actually applied):
jacocoAndroidUnitTestReport {
excludes += [
'**/*_MembersInjector.class',
'**/Dagger*Component.class',
'**/Dagger*Component$Builder.class',
'**/*Module_*Factory.class'
]
}
Connected tests
If you're running android connected test coverage by setting testCoverageEnabled true in your buildType, unfortunately there is no idiomatic way to declare excludes, since the android gradle plugin doesn't provide such options, and the predefined jacoco report task has the excludes hardcoded. In this case, you have to script your own task with excludes.
IntelliJ test runner
If you're using the IntelliJ test runner, whether the coverage is done by IntelliJ or JaCoCo, you need to put the includes for a test configuration.
Open the Edit Configurations window:
Choose your test config and define includes (classes or whole packages). In this case I included the whole com.google.android.gms package, just as an example:
To exclude dagger generated files, the quickest way is to put all the dagger dependencies in one root package, and include all the other packages in the test configuration.
Exclude files from AndroidStudio index
After many days I found solution: exclusion files from IDE index also exclude them from IDE's code coverage report.
So we need create new File-Type for all codegen files (or only Dagger/Hilt files), and exclude this File-Type from index.
To achieve this you need:
1. Create new File-Type 'Codegen' for codegen-files
Go to Preferences -> File Types, and add new file-type Codegen:
Add this templates (templates may change in future):
*_*Factory.java
*_ComponentTreeDeps.java
*_Factory.java
*_GeneratedInjector.java
*_HiltComponents.java
*_HiltModules.java
*_HiltModules_BindsModule.java
*_HiltModules_KeyModule.java
*_MembersInjector.java
*_ProvideFactory.java
*_SingletonC.java
*_TestComponentDataSupplier.java
BR.java
BuildConfig.java
DataBinderMapperImpl.java
Hilt_*.java
_test_*.java
2. Exclude 'Codegen' from index
Open Go to File, choose tab Files and then click Filter and uncheck Codegen file-type.
That's all!
After this when you will run tests with code coverage (with IntelliJ IDEA runner), matched Codegen-files will be excluded also from code coverage report in IDE.
Note: With this approach you can't get coverage from CLI. If you need get coverage from CLI - use Jacoco.
More recent versions of Dagger generate additional files with slightly different patterns. I had success with the following excludes when using Dagger 2.15
'**/*_MembersInjector.class',
'**/Dagger*Component*.class',
'**/Dagger*Subcomponent*.class',
'**/*Subcomponent$Builder.class',
'**/*Module_*Factory.class',

Configure test folder for unit testing in Android studio

I have added a folder for unit testing in my android studio project. The default folder is andoidTest but I have added a new folder and name in test instead. (like robolectric sample tests)
When I add test Dependency in my build.gradle under module such as
testCompile("junit:junit:${junitVersion}")
testCompile ("org.robolectric:robolectric:${robolectricVersion}")
They do not get added to external libraries under project, but when I use the default configuration and use androidTestCompile, it can added external libraries.
Then I thought that maybe I should setRoot for tests in gradle, so I used following in android tag in build.gradle:
sourceSets {
androidTest.setRoot('src/test')
}
But still problem remained. I can run tests using gradlew, but imports in classes in test folder do not apply as well as no external library for test purpose is visible.
Anyone have any solution for this issue?
I was searching and didn't find answer that I thought already covered this. So decided to create new one for the future.
Answer
Android Studio is not picking up unit tests automatically right now. I know it is planned for 1.3 version.
So you have to change test artifact value from Android Instrumental Tests to Unit Tests in Build Variants tool window:
Almost fine your Gradle script but try do that:
sourceSets {
androidTest.setRoot('src/test')
androidTest {
java.srcDirs = ['src/test/java']
}
}

How to add a new source directory to an Android Studio project?

So ultimately I'm trying to separate my integration tests from the unit tests in an Android Studio project. I've found a few resources on the subject:
http://selimober.com/blog/2014/01/24/separate-unit-and-integration-tests-using-gradle/
https://blog.safaribooksonline.com/2013/08/22/gradle-test-organization/
Separating integration tests from unit tests in Android Studio
All these seem to indicate that the way to go is to create a new sourceSet for the integration tests, and then to create a new test task which builds and runs the tests in that source set. I can't get past the first step of creating a source set which is recognized by Android Studio.
Here's what I have within app/build.gradle, which builds without errors, but does not result in an integrationTest source root I can add classes to:
android{
...
sourceSets{
integrationTest {
java.srcDir('src/integrationTest/java')
}
}
}
My questions are:
Where precisely do I have to add the sourceSets block? In build.gradle? in app/build.gradle? In app/build.gradle inside the android block?
Once I've added my source set in he right place using the correct syntax, is this sufficient for Android Studio to detect and present it in the UI along side the main and test sources, or are there additional steps?
edit:
I've attempted to follow the instructions in marius' answer, but integrationTest isn't showing up in my build variants. Here's what I'm seeing:
This is enough:
android{
...
productFlavors{
integrationTest {
}
}
}
Regarding your 1st question: The productFlavors block should be in your app/build.gradle, inside android block.
Regarding your 2nd question: Once you add this to your build.gradle file, you also need to create your folders /src/integrationTest and /src/integrationTest/java . Once that is done, sync your gradle files and choose your new Build Variant from the Build Variant window, in order for the IDE to detect it as the active source folder.

Categories

Resources