After setting up Jenkins to auto build and deploy my IRC bots, I decided to add the next component of a CI/CD stack, automated testing. This was to use ant to run the tests, and JUnit for the testing framework for the application. I’ll be setting this up in both of my Jenkins projects, I have one that just builds my applications, and a second that builds the docker containers and pushes them to the repository.
My Jenkins setup includes 2 builds for each of my projects:
- Java Build
- Builds the java application
- This is just a general Jenkins project that uses ant to build
- Docker Build
- Builds the docker container, tags it, and pushes it to my local docker repository
- This uses Jenkins pipelines to perform the build, tag, push
The docker build pushes my production code and is used by my docker-swarm to update my locally built containers. This is the important build. The general java build is just me experimenting with Jenkins builds following the non-pipeline route.
Ant Script
Just including the changes to my ant script below, the entire thing can be seen on GitHub. This adds 2 primary components, a method to compile the tests, and a method to run them. Running the tests is of course dependent on compiling the tests first. I did encounter some issues when getting this going, in part due to my dev environment ant wanting to run on openjdk-11, while my codebase is built and coded for openjdk-8. Once I had that worked out, running “ant unit-test-all” worked perfectly and gave me the confidence to push the changes to git and incorporate the new targets into my Jenkins configurations.
<property name="src.dir" value="src"/> <property name="src.test" value="test"/> <property name="reports.tests" value="test-reports"/> <property name="build.dir" value="build"/> <property name="classes.dir" value="${build.dir}/classes"/> <property name="test.classes.dir" value="${build.dir}/testclasses"/> <property name="lib.dir" value="lib"/> <property name="test.dir" value="testlib"/> <path id="test.classpath"> <fileset dir="${lib.dir}" includes="**/*.jar"/> <fileset dir="${test.dir}" includes="**/*.jar"/> </path> <target name="compileTest" depends="compile" description="compile jUnit Test cases "> <mkdir dir="${classes.dir}"/> <echo message="Ant java version: ${ant.java.version}" /> <javac srcdir="${src.test}" destdir="${classes.dir}" includeantruntime="true" nowarn="yes" debug="true" classpathref="test.classpath"/> </target> <target name="unit-test-all" depends="compileTest"> <mkdir dir="${reports.tests}"/> <junit printsummary="yes" haltonfailure="yes"> <classpath> <pathelement location="${classes.dir}"/> <pathelement location="${classes.dir}"/> <fileset dir="testlib" includes="**/*.jar"/> <pathelement path="${java.class.path}"/> </classpath> <formatter type="plain"/> <formatter type="xml"/> <batchtest fork="yes" todir="${reports.tests}"> <fileset dir="${src.test}"> <include name="**/*Test*.java"/> </fileset> </batchtest> </junit> </target>
The unit-test-all target will compile the tests, build a classpath using the test and production classes, and then run all classes that contain “Test” in the name. The key point here is that I had to compile my tests into the same classpath that the source code was compiled into. Maybe I missed something in my configuration to use a test classpath separate from source, however this method worked overall.
I set this up to run any classes that contain the word “Test” in them, however this may be better off to only run classes that end in test, or to provide a simple list of test suites to specifically run. For now though, this over aggressive method will work. This will ensure my tests are run and that everything is working as expected. Since this is an IRC bot and never had any tests before, its currently quite slim on the cases, however this was a good experiment on getting tests running automated in Jenkins.
Project Library Changes
I ended up adding a testlib folder to my project, alongside the lib folder. The testlib folder contains all the jars needed to run the test cases. This is currently only JUnit and hamcrest. This library folder is put alongside the original lib folder as a way to separate testing libraries vs production libraries, avoiding misuse of APIs in the production codebase.
I realize this isn’t best practice, storing the jars in the git repo and building using them, however for this small project, it works fine. This project is used for a lot of learning and experimenting (like Jenkins, docker builds, and the bot was originally written to learn java). So in this vein, moving the project to handle libraries in a method following best practices will be a project for another day. For now, we keep the libraries in our repo for the entire build pipeline to use.
Java Build
For my java build setup, I use ant tasks for the build and test, and then run a post build action on the test reports generated by JUnit.
- Invoke Ant
- Targets: clean-build
- Invoke Ant
- Targets: unit-test-all
- Publish JUnit test result report
- Test report XMLs: test-reports/*.xml
- Health report amplification factor: 1.0
The publish JUnit action is in the post build/test of the project. This gathers the test data and reports it to Jenkins, giving the graph of passed/failed/skipped test cases. With this, Jenkins can then be configured on when to fail the build based off of failing test cases.
Docker Pipeline
My docker pipeline runs in a similar way to my non-pipeline project. this runs the ant testing using a shell call:
ant unit-test-all
This then analyzes the test reports using the Jenkins pipeline built in JUnit task:
junit 'test-reports/*.xml'
This should look familiar to the java build above, as its nearly a drop in copy. Running this extra stage in my docker pipeline allows my pipeline to: build, test, and push the production docker image if everything passes. Combining this with shepherd running on my cluster, I produce a full CI/CD pipeline.
pipeline { environment { registry = "127.0.0.1:5000/wheatley" } agent any stages { stage('Git Pull') { steps{ git 'https://github.com/AeroSteveO/Wheatley.git' } } stage('Ant Test') { steps { sh "ant unit-test-all" junit 'test-reports/*.xml' } } stage('Building image') { steps{ script { dockerImage = docker.build registry + ":latest" } } } stage('Deploy Image') { steps{ script { docker.withRegistry( '' ) { dockerImage.push() } } } } stage('Remove Unused docker image') { steps{ sh "docker rmi $registry" } } } }
As a side note, the stage to remove the unused docker image just removes the locally built image, leaving the image in the repository (ensuring the system building the images doesn’t become over encumbered in images).
Conclusion
This has been a great way to learn how to setup a CI/CD pipeline, allowing me to run testing before building and pushing my docker images for my IRC bots. While I only have 4 tests thus far in my bot, this will help me setup future pipelines that make use of more extensive testing, and I can add further tests to my bots and other projects this way as well. Even with this small experiment and introduction into the automated testing pipeline, I found some minor bugs in my codebase, while they haven’t caused issues in the years I’ve run this bot, this does show the use of having a system like this.