JaCoCo Code Coverage Instrumentation Principles and Practices for Android SDK
The article explains how to use JaCoCo to instrument Android SDK classes—detailing probe insertion strategies for methods, branches, and code blocks, offline and Ant/Gradle integration, building instrumented packages, collecting execution data, and generating HTML coverage reports without modifying source code.
High‑definition map platform products are evolving rapidly, making the existing test process insufficient to guarantee full coverage of all business code. To ensure comprehensive testing, code coverage is introduced as a test metric, requiring SDK code to be instrumented (colored) so that a coverage report can be generated after testing.
JaCoCo Tool
JaCoCo has several advantages:
Supports both Ant and Gradle packaging, allowing flexible switching.
Supports offline mode, which fits SDK usage scenarios.
Documentation is comprehensive and actively maintained.
JaCoCo works by using ASM to manipulate Java bytecode and insert probes. The following sections describe the probe insertion principles.
JaCoCo Probe Insertion
Because Java bytecode is a linear sequence of instructions, JaCoCo inserts special code (probes) at required locations using ASM.
Example of instrumenting a simple method:
// original java method
public static int Test1(int a, int b) {
int c = a + b;
int d = c + a;
return d;
}
//-------------------------- separator ----------------------------
// JaCoCo‑instrumented method
private static transient /* synthetic */ boolean[] $jacocoData;
public static int Test1(final int a, final int b) {
final boolean[] $jacocoInit = $jacocoInit();
final int c = a + b;
final int n;
final int d = n = c + a;
$jacocoInit[3] = true;
return n;
}
private static boolean[] $jacocoInit() {
boolean[] $jacocoData;
if (($jacocoData = TestInstrument.$jacocoData) == null) {
$jacocoData = (TestInstrument.$jacocoData = Offline.getProbes(-6846167369868599525L, "com/jacoco/test/TestInstrument", 4));
}
return $jacocoData;
}The instrumented code shows Boolean arrays and probe assignments added automatically.
JaCoCo records coverage by marking entries in these Boolean arrays (probes) whenever the corresponding bytecode is executed.
Probe Insertion Strategy
Four main strategies are used:
Track method execution.
Track branch execution.
Track ordinary code block execution.
Method Execution
Probes are added at method entry or exit. Adding at the method tail can capture all return/throw paths but is more complex; adding at the head is simpler but only indicates that the method was entered.
public void visitInsn(final int opcode) {
switch (opcode) {
case Opcodes.IRETURN:
case Opcodes.LRETURN:
case Opcodes.FRETURN:
case Opcodes.DRETURN:
case Opcodes.ARETURN:
case Opcodes.RETURN:
case Opcodes.ATHROW:
probesVisitor.visitInsnWithProbe(opcode, idGenerator.nextId());
break;
default:
probesVisitor.visitInsn(opcode);
break;
}
}Branch Execution
JaCoCo reverses conditional jumps (if‑else) to simplify probe insertion. For example, a simple if‑statement is transformed:
// source
public static void Test4(int a) {
if (a > 10) {
a = a + 10;
}
a = a + 12;
}
// JaCoCo‑instrumented bytecode
public static void Test4(int a) {
boolean[] var1 = $jacocoInit();
if (a <= 10) {
var1[11] = true;
} else {
a += 10;
var1[12] = true;
}
a += 12;
var1[13] = true;
}Reversing the condition allows JaCoCo to insert a probe for each branch without adding an explicit else block.
Code Block Execution
Probes are inserted before method‑call instructions and before labels that have multiple incoming jumps. This avoids a performance penalty of probing every sequential instruction.
public static void Test6(int a, int b) {
boolean[] var2 = $jacocoInit();
a += b;
b = a + a;
var2[19] = true;
Test();
int var10000 = a + b;
var2[20] = true;
Test();
var2[21] = true;
}Special Cases Q&A
Q: Why is a probe inserted at the end of an else block? A: The label before the else block has two sources (a GOTO and sequential flow), satisfying the multi‑target condition, so a probe is added.
Q: Why is no probe inserted before the first Test() call in a case block? A: The preceding instruction is a GOTO, making successor = false, so no probe is needed.
Conclusion of Probe Insertion
Insert probes before return and throw statements.
For complex if statements, reverse the condition and insert probes for each branch.
Insert a probe before a method call when the instruction is a successor of the previous one.
Insert a probe when the current instruction is a successor and has multiple incoming jumps.
Building an Instrumented SDK Package
Use JaCoCo’s Ant plugin to instrument compiled classes:
<project name="Example" xmlns:jacoco="antlib:org.jacoco.ant">
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="path_to_jacoco/lib/jacocoant.jar"/>
</taskdef>
...
<jacoco:instrument destdir="target/classes-instr" depends="compile">
<fileset dir="target/classes" includes="**/*.class"/>
</jacoco:instrument>
</project>Test Project Configuration
Place the instrumented JAR in the test project’s libs directory and enable coverage in build.gradle: testCoverageEnabled = true // enable JaCoCo coverage Coverage data can be collected via the JaCoCo agent properties file or by invoking org.jacoco.agent.rt.RT.getAgent().getExecutionData() via reflection to write an .ec file.
public static void generateEcFile(boolean isNew, Context context) {
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if (!file.exists()) { file.mkdir(); }
DEFAULT_COVERAGE_FILE = DEFAULT_COVERAGE_FILE_PATH + File.separator + "coverage-" + getDate() + ".ec";
Log.d(TAG, "Generating coverage file: " + DEFAULT_COVERAGE_FILE);
OutputStream out = null;
File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE);
try {
if (!mCoverageFilePath.exists()) { mCoverageFilePath.createNewFile(); }
out = new FileOutputStream(mCoverageFilePath.getPath(), true);
Object agent = Class.forName("org.jacoco.agent.rt.RT").getMethod("getAgent").invoke(null);
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class).invoke(agent, false));
Log.d(TAG, "Write completed!");
Toast.makeText(context, "Write " + DEFAULT_COVERAGE_FILE + " completed!", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e(TAG, "generateEcFile: " + e.getMessage());
} finally {
if (out != null) {
try { out.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
}Generating Coverage Reports
JaCoCo can merge multiple .exec files and produce an HTML report via Ant:
<jacoco:merge destfile="merged.exec">
<fileset dir="executionData" includes="*.exec"/>
</jacoco:merge>
<jacoco:report>
<executiondata>
<file file="jacoco.exec"/>
</executiondata>
<structure name="Example Project">
<classfiles>
<fileset dir="classes"/>
</classfiles>
<sourcefiles encoding="UTF-8">
<fileset dir="src"/>
</sourcefiles>
</structure>
<html destdir="report"/>
</jacoco:report>Understanding Java bytecode, ASM, and JaCoCo’s instrumentation logic enables advanced SDK manipulation without source changes.
Reference: JaCoCo Documentation
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Amap Tech
Official Amap technology account showcasing all of Amap's technical innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
