7

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

Ermintar
  • 1,322
  • 3
  • 22
  • 39
  • 2
    This question was already solved [here](https://stackoverflow.com/a/31916686/7096763), but I don't want to answer because this other answerer deserves all the credit. I managed to make it work using the last block of code in the linked answer, all you have to do is replace `classesDir` with `classesDirs[0]`, I'm guessing because of an API change. – MikaelF May 21 '20 at 17:54
  • @MikaelF, link you suggested works for a single-module project. I will check if it can be tuned for multimodule project (with merging source sets) also. – Ermintar May 21 '20 at 18:40
  • @MikaelF, checked your solution in multimodule. The instrmentation block has to be published inside subpropjects declaration and project.{expression} replaced with subproject.{expression}. If you are not up to a bounty, I'll post a subproject example, after it expires – Ermintar May 28 '20 at 15:00
  • Please do. I'm glad I could help, I just don't like taking credit for other people's work ;) Also, it's a pretty ugly solution, it really should be implemented at the plugin level, it's too much code to maintain (and break) in a build file IMO. – MikaelF May 28 '20 at 15:05

2 Answers2

3

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): jacoco test report

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

gybandi
  • 1,880
  • 1
  • 11
  • 14
  • 1
    Basic difference is that your maven config has stage prepare-agent. As far as I know, gradle jacoco plugin doesn't have one. It has opions to configure javaAgent, but I'm not sure wich should be used if any – Ermintar May 14 '20 at 08:36
  • Here's my sample https://github.com/lizardeye/jacocosample/tree/master/src/main/java/sample – Ermintar May 14 '20 at 08:37
  • 1
    sadly I haven't found a viable solution for gradle yet. I'll post another bounty after the current one expires, maybe someone else will have the solution – gybandi May 20 '20 at 09:58
  • 1
    @gybandi see my comment on the question for a solution for Gradle. – MikaelF May 21 '20 at 18:06
  • which gradle we need to include this app level of project level , I am using android studio and checking in android app. Please help me with some guidence. – Surya Reddy Feb 19 '22 at 12:45
1

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

Ermintar
  • 1,322
  • 3
  • 22
  • 39