Collecting Code Coverage for a Jenkins Shared Library
Part Four of my series on Jenkins Shared Libraries
In my previous post, we walked through how you can write unit tests for a Jenkins Shared Library. In the fourth and final entry into my Jenkins Shared Library series I am going to show you how you can make a few small changes to your shared library which would allow you to collect code coverage and publish the results to Sonarqube.
Prerequisites
For this tutorial we are going to need to have Sonarqube installed locally. You can follow the instructions that are provided here. Look for the instructions under “Get Started in Two Minutes Guide”. This should give you a running Sonarqube instance at http://localhost:9000. You can login with the default username admin
and password admin
.
The next thing we need to do is install the SonarQube Groovy plugin. Once you have logged in, navigate to http://localhost:9000/admin/marketplace. You will see a screen like this:
In the search box under plugins, search for groovy
and then click the install
button to the right of the plugin.
When you click Install
, you will see a message telling you to restart Sonarqube.
Click Restart
.
Sonarqube will restart and then you will be asked to login again. Use the default username and password from earlier. Once that is done, Sonarqube is now ready to go!
Getting Started
This tutorial picks up where part three of the series left off. If you have not been following along I recommend going back to part one of the series or you can clone the shared library from https://github.com/werne2j/jenkins-shared-library. We are going to be continuing to work off of the tests
branch. Run git checkout tests
and that will put you right where we are starting from.
Collecting Code Coverage
We are going to be using JaCoCo to collect code coverage for our shared library. This is going to require a few changes to our pom.xml
file.
The first thing we are going to add is some properties that are needed for Sonarqube. Under the properties
tag we will add:
<properties>...<sonar.projectName>jenkins-shared-library</sonar.projectName>
<sonar.projectKey>jenkins-shared-library</sonar.projectKey>
<sonar.sources>src,vars</sonar.sources>
<sonar.tests>test</sonar.test>
<sonar.groovy.jacoco.reportPath>target/jacoco.exec</sonar.groovy.jacoco.reportPath>
</properties>
This tells Sonarqube what project to report to with projectName
and projectKey
. Where the code is to analyze and where the tests are located. And lastly, where to look for the code coverage analysis.
Next, we need to add the JaCoCo plugin under the pluginManagement —> plugin
tag.
<build>
<pluginManagement>
<plugins>
...
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.3</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
...
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
This makes the plugin available to be used and tells the plugin to run during the test
phase. So when we run mvn test
, a coverage report will be created.
Thats it!
Lets tests collecting code coverage and see what we get!
We are going to run mvn clean test
to generate a JaCoCo report.
We should get an output like this:
If you look down under the output of the tests, you will see the jacoco-maven-plugin
generating a coverage report. And now we will send this report to Sonarqube and see what the results are.
Now, in your terminal, run mvn sonar:sonar
. This will analyze the project and report its results to Sonarqube. When the command finishes you can navigate to http://localhost:9000/dashboard?id=jenkins-shared-library and see the results from the analysis.
But wait, something doesn’t look right — we don’t see any code coverage. This is actually expected. If you look at this comment in a Jenkins-Spock issue, it talks about how collecting coverage is not easily done.
However, through my trials and tribulations of trying to collect code coverage, I was able to come up with a solution to this issue.
Restructuring the shared library
In order to collect code coverage for our shared library, we are going to move the logic of our global variables into the src
directory.
We are going to be creating two classes BuildJavascriptApp
and Notify
inside src/org/example
. And to match that we are going to create two new test files BuildJavascriptAppSpec
and NotifySpec
, these are going to be located in test/src/org/example
. The directory structure should now look like:
├── jenkins-shared-library
│ ├── src
│ │ ├── org
│ │ │ ├── example
│ │ │ │ ├── BuildJavascriptApp.groovy
│ │ │ │ ├── Constants.groovy
│ │ │ │ ├── Notify.groovy
│ ├── test
│ │ ├── resources
| │ ├── src
│ │ | ├── org
│ │ │ | ├── example
│ │ │ │ | ├── BuildJavascriptAppSpec.groovy
│ │ │ │ | ├── NotifySpec.groovy
│ │ ├── vars
│ ├── vars
│ │ ├── buildJavascriptApp.groovy
│ │ ├── notify.groovy
Taking a look at our classes, they are going to look very similar to what the global variables look like. For example, here is what BuildJavascriptApp.groovy
will look like:
package org.exampleclass BuildJavascriptApp {
def exec(Map config=[:], Closure body={}) {
node {
git url: "https://github.com/werne2j/sample-nodejs" stage("Install") {
sh "npm install"
} stage("Test") {
sh "npm test"
} stage("Deploy") {
if (config.deploy) {
sh "npm publish"
}
} body()
}
}
}
We are taking the global variable and wrapping it in an exec
method inside of a class.
Now for our global variable, we are going to update it to call our new class.
import org.example.BuildJavascriptAppdef call(Map config=[:], Closure body={}) {
def buildJavascriptApp = new BuildJavascriptApp()
buildJavascriptApp.exec(config, body)
}
And now our global variable is very simple and is only calling our new class.
The last thing to do is update the test. We are going to copy the tests from test/vars/BuildJavascriptAppSpec.groovy
to the new test file we created at test/src/org/example/BuildJavascriptAppSpec.groovy
. The only thing we have to do is make a couple small adjustments. We will be loading in the class instead of the variable, since we moved all the logic to the class. And then we will be calling the exec
method instead of calling the variable. The code should look like this:
import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification
import org.example.BuildJavascriptAppclass BuildJavascriptAppSpec extends JenkinsPipelineSpecification {
def buildJavascriptApp = null def setup() {
buildJavascriptApp = new BuildJavascriptApp()
} def "[buildJavascriptApp] will run npm publish if deploy is true"() {
when:
buildJavascriptApp.exec(deploy: true)
then:
1 * getPipelineMock("sh")("npm publish")
} def "[buildJavascriptApp] will not npm publish if deploy is false"() {
when:
buildJavascriptApp.exec(deploy: false)
then:
0 * getPipelineMock("sh")("npm publish")
} def "[buildJavascriptApp] will call closure if passed"() {
setup:
def body = Mock(Closure)
when:
buildJavascriptApp.exec(deploy: false, body)
then:
1 * body()
}
}
You can now delete the previous test residing in the test/vars
directory. That is the last thing we need to do to convert the code to be able to collect code coverage. I will let you try your hand at converting the notify
variable to have the logic in a class like we did with buildJavascriptApp
.
There is a complete example at https://github.com/werne2j/jenkins-shared-library/tree/sonarqube if you would like to see the final results.
Once the library has be restructured, we are going to re-run the tests and try to reanalyze the library with Sonarqube.
Again, we will run mvn clean test
.
We see that the tests have succeeded and our new report has been generated.
Now let’s run mvn sonar:sonar
one more time. When the command finishes running navigate back to http://localhost:9000/dashboard?id=jenkins-shared-library. Or refresh the page if you still have it up. And we should see something new.
And there we have it, coverage!
Conclusion
With this strategy, we can now collect code coverage on a Jenkins Shared Library. This is great for managers or team leads that want to see whether or not code coverage is increasing or decreasing or being collected at all.
Thank you for reading part four of my Jenkins Shared Library series.
I really hope that you have enjoyed my series on Jenkins Shared Libraries. We have gone from explaining what the libraries are and how to use them, to writing tests and then collecting code coverage.
Hopefully you can take the knowledge that you’ve learned from this series and apply it to your work or personal endeavors.
Thank you again and good luck with all your Jenkins Shared Library adventures!