I'm testing an app with JUnit5 and using Jacoco for coverage report. Tests are executed Ok and test reports are present.
However, Jacoco report has the following logs if service contains methods, annotated with @Transactional
[ant:jacocoReport] Classes in bundle 'my-service' do no match with execution data. For report generation the same class files must be used as at runtime.
[ant:jacocoReport] Execution data for class mypackage/SampleService does not match.
This error occurres for all @Service classes methods, annotated with @Transactional, plain classes coverage is calculated ok.
Here's a sample test:
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class MyServiceTest {
@Autowired
private SampleService sampleService;
@Test
public void doWork(){
sampleService.doWork();
}
}
Works fine. Coverage is non-zero:
public class SampleService {
public void doWork(){
System.out.println("HEY");
}
}
0% coverage:
public class SampleService {
@Transactional
public void doWork(){
System.out.println("HEY");
}
}
Transactional creates a proxy around actuall class. But, isn't there an out-of-box way for Jacoco to handle such a common situation?
I've tried @EnableAspectJAutoProxy annotaion with different flag variations, checked that up-to-date Jupiter engine and Jacoco plugin are used
Here's gradle config:
subprojects {
test {
useJUnitPlatform()
}
jacocoTestReport {
afterEvaluate {
classDirectories.from = files(classDirectories.files.collect {
fileTree(dir: it, exclude: '*Test.java')
})
}
reports {
html.enabled = true
xml.enabled = true
csv.enabled = false
}
}
}
Any help appreciated
I tried it with a test project, similar to what you've described, however I couldn't reproduce the issue. The only difference that I see between your project and mine, is that I've used maven instead of gradle.
Here is the test project: https://github.com/gybandi/jacocotest
And here is the jacoco result for it (using org.springframework.transaction.annotation.Transactional annotation):
If this doesn't help you, could you upload your test project to github or some other place?
Edit: @MikaelF posted a link to another answer, which shows how to add offline instrumentation for jacoco.
The solution that was described there worked for me, after I added the following block to build.gradle:
task instrument(dependsOn: [classes, project.configurations.jacocoAnt]) {
inputs.files classes.outputs.files
File outputDir = new File(project.buildDir, 'instrumentedClasses')
outputs.dir outputDir
doFirst {
project.delete(outputDir)
ant.taskdef(
resource: 'org/jacoco/ant/antlib.xml',
classpath: project.configurations.jacocoAnt.asPath,
uri: 'jacoco'
)
def instrumented = false
if (file(sourceSets.main.java.outputDir).exists()) {
def instrumentedClassedDir = "${outputDir}/${sourceSets.main.java}"
ant.'jacoco:instrument'(destdir: instrumentedClassedDir) {
fileset(dir: sourceSets.main.java.outputDir, includes: '**/*.class')
}
//Replace the classes dir in the test classpath with the instrumented one
sourceSets.test.runtimeClasspath -= files(sourceSets.main.java.outputDir)
sourceSets.test.runtimeClasspath += files(instrumentedClassedDir)
instrumented = true
}
if (instrumented) {
//Disable class verification based on https://github.com/jayway/powermock/issues/375
test.jvmArgs += '-noverify'
}
}
}
test.dependsOn instrument
There seems to be an open ticket on the jacoco plugin's github about this as well: https://github.com/gradle/gradle/issues/2429
Based on single module instrumentation example https://stackoverflow.com/a/31916686/7096763 (and updated version for gradle 5+ by @MikaelF) here's example for multimodule instrumentation:
subprojects { subproject ->
subproject.ext.jacocoOfflineSourceSets = [ 'main' ]
task doJacocoOfflineInstrumentation(dependsOn: [ classes, subproject.configurations.jacocoAnt ]) {
inputs.files classes.outputs.files
File outputDir = new File(subproject.buildDir, 'instrumentedClasses')
outputs.dir outputDir
doFirst {
project.delete(outputDir)
ant.taskdef(
resource: 'org/jacoco/ant/antlib.xml',
classpath: subproject.configurations.jacocoAnt.asPath,
uri: 'jacoco'
)
def instrumented = false
jacocoOfflineSourceSets.each { sourceSetName ->
if (file(sourceSets[sourceSetName].output.classesDirs[0]).exists()) {
def instrumentedClassedDir = "${outputDir}/${sourceSetName}"
ant.'jacoco:instrument'(destdir: instrumentedClassedDir) {
fileset(dir: sourceSets[sourceSetName].output.classesDirs[0], includes: '**/*.class')
}
//Replace the classes dir in the test classpath with the instrumented one
sourceSets.test.runtimeClasspath -= files(sourceSets[sourceSetName].output.classesDirs[0])
sourceSets.test.runtimeClasspath += files(instrumentedClassedDir)
instrumented = true
}
}
if (instrumented) {
//Disable class verification based on https://github.com/jayway/powermock/issues/375
test.jvmArgs += '-noverify'
}
}
}
test.dependsOn doJacocoOfflineInstrumentation
}
full example here: https://github.com/lizardeye/jacocomultimodulesample
Still, I think this is a durty hack, which can be easily broken with gradle or jacoco updates
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With