Using automation to build, test and deploy our code
Overview
Teaching: 20 min
Exercises: 60 minQuestions
How do we use CESSDA’s infrastructure to automatically build, test and deploy our code?
Objectives
Understand the benefits of automation in software development.
Use CESSDA’s Jenkins Continuous Integration server to build and test exemplar code.
Write unit tests for exemplar code.
Contribute unit tests upstream to official repository.
Building on the code we’ve written and placed into Git, in this episode we will see how CESSDA’s infrastructure can help us to to automatically build, test, and deploy the code we’ve written.
Automation in software development
Automating as much as you can in software development is a best practice goal for a number of reasons. A good rule of thumb is if you find yourself doing something manually more than once than can be automated, strongly consider automating it.
Question: What are the benefits of automation?
What do you think are the main benefits of automation?
Some general benefits include:
- Productivity: automating things like building and testing your code saves you time, allowing you to focus on other things.
- Improvement: running automated tests frequently increases the chances you’ll find and fix errors earlier, leading to better software.
- Reproducibility: having a well-defined and automated process for building and testing your code ensures you (and others) don’t make mistakes, leading to potentially faulty code and incorrect results.
- Reuse: automation not only makes it easier for others to quickly build, test, deploy, extend, and reuse your software, the automation itself acts as instructive documentation for how to understand how these things should be done.
The main steps, as a ‘holy grail’ of what to achieve, are:
- Provide automated build process: far easier and quicker to validate changes, e.g. use of Make, Ant, Maven.
- Provide a set (or suite) of unit tests: to check if changes to your code break anything, e.g. JUnit, CPPUnit, xUnit, fUnit, …
- Join together - automated build & test: a ‘fail fast’ environment for community development.
- Use Continuous Integration (CI): automate the building, testing, and even deployment, of your code as changes are made.
How CI works in practice is that:
- You make and commit code changes to your repository.
- A CI server is configured to notice these commits and independently checks out your code from revision control and performs a number of predefined steps automatically (like build, test, deploy).
- Presents you with a report on the progress, along with success or failure, of these tasks as well as any errors encountered.
This is why continuous integration helps your software to always be releasable: tests are run in response to changes to the code, and you are notified quickly when tests fail so that you can correct the reason for the failure. It is easier to fix a bug in something you wrote a few minutes ago, than something you wrote yesterday (or last week, or last month).
For getting your software accepted by CESSDA, you’re expected to use CI from the start of the development process. This means you’re already thinking about and implementing code quality from the outset. This also means CESSDA can inspect test results, as everything is already within the CESSDA environment.
Jenkins within CESSDA
Jenkins is a popular, extensible, open source continuous integration server. By associating a Jenkins ‘job’ with a code repository, it can automatically build, test, and even deploy, your software in the background.
This is not intended to be a complete introduction to everything that Jenkins offers. Rather, it is hoped that it will serve to get you started, helping you through your first steps of using Jenkins, to give an idea as to its potential and usefulness and how you can use it within CESSDA.
There are many ways to use Jenkins to do things like build, test and deploy the code in your repository. For instance, you can configure Jenkins to run a specific command, e.g. ant
to run the Ant commands present in a build.xml
file.
Within CESSDA, a Jenkins job is preconfigured for your repository when the repository is created, and it helpfully uses a more modern Jenkins approach, which gives you more flexibility on what is done by the Jenkins job. This approach is achieved via a Jenkins Pipeline, where you define the commands you’d like Jenkins to run within a Jenkinsfile. You can either define these commands in one of two ways:
- Declaratively: where you define what the Jenkins job needs to do at a high-level.
- Scripted Pipeline: where you have more flexibility, and can use the Groovy syntax, complete with conditionals, loops, etc. to have more finely-grained and explicit control over what the Jenkins job does.
For the purposes of this training, we’ll be concentrating on the declarative approach to build and test our code. But this should give you a solid foundation for exploring the more advanced features offered by using a scripted pipeline.
Using Jenkins to build and test our code
So a Jenkinsfile isn’t used as a replacement to how we build and test our code, but rather it invokes a suitable build system underneath to do these things. For our example code, we used Ant, so we need to define these actions within our Jenkinsfile.
A Jenkinsfile is structured around a pipeline, which can include:
- At least one agent, which defines the type of environment that the Jenkins job will run within. This can be things like docker, if you want to use a particular docker container for example. If you supply none, you can define separate agents for each stage of the pipeline.
- A number of stages, which each stage defining a number of steps, typically a sequence of commands, you’d like to run within the Jenkins job to accomplish that stage.
Let’s start by looking at the Jenkinsfile we have in the root directory of our repository:
$ cat Jenkinsfile
pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Building..'
sh 'mvn clean compile'
}
}
stage('Test') {
steps {
echo 'Testing..'
sh 'mvn test'
}
}
}
}
For simplicity, here we’re specifying that we don’t mind which type of agent will run our Jenkins job. Within the stages, our ‘Build’ stage simply instructs Maven to build (compile) our Java code, and our ‘Test’ stage invokes Maven to run our tests.
Jenkins - a brief tour
So in the background, pull requests that were merged into the official repository started off a new Jenkins job within CESSDA’s Jenkins infrastructure. The Jenkins job has cloned the official repository following that change to it, and run the Jenkins job based on the pipeline specified in our Jenkinsfile.
Let’s take a look at the results of the latest Jenkins job now, by going to the official repo at https://bitbucket.org/cessda/cessda.ces2018.test, and selecting ‘Branches’ from the navigation bar.
We’ll see our two branches Master and Develop, but also note the icons in the ‘Builds’ column. Jenkins is running a build job against both of these branches in the background, and these results are linked to back here. You’ll notice our Master branch (which still contains our faulty Fibonacci code) has a failed build, as we would expect, but the Develop branch now has a successful build!
The successful Jenkins build
Let’s look at the successful build first by clicking on that green icon, where we’ll be taken to the results of that Jenkins job. We can see that the stages in our Jenkinsfile pipeline, ‘Build’ and ‘Test’, have both been successful. Below, we have access to more details.
Select ‘Build’ from the pipeline, and we can see results for the following successes:
- The repository clone (under ‘General SCM’)
- The build process (under ‘Building…’ and ‘mvn clean compile’)
By inspecting each, we can see the console output from each of the steps.
Our Jenkins job also runs the ‘Test’ stage. It turns out that we already have a test in our repository for our code, that we’ll look at later. So similarly, if we select ‘Test’ from the pipeline at the top, we can see results for:
- The test process (under ‘Testing’ and ‘mvn test’)
The failed Jenkins build
If we now select the failed Jenkins build from the official repository branches page, it’s a similar but different story. We can see that whilst the ‘Build’ stage was successful, the ‘Test’ one failed, showing the console error we would expect in the console output.
At least our Develop branch was successful. But of course, we only have one test. Is this enough?
Unit testing
Confess!
Why don’t you write tests?
- “I don’t write buggy code”
- “It’s too hard”
- “It’s not interesting”
- “It takes too much time and I’ve research to do”
What testing gives you:
- Confidence that your code does what it is supposed to
- That your research is built on a solid foundation
- Ability to detect, and fix, bugs more quickly
- Correct code (bugs caught early in the cycle)
- Confidence to refactor or fix bugs without creating new bugs
- Examples of how to use your code
Wise words
“If it’s not tested, it’s broken” - bittermanandy, 10/09/2010
Examples of unit testing frameworks:
- Fortran: FRUIT, pFUnit
- R: RUnit, testthat
- MATLAB: Unit Testing Framework
- .NET: csUnit
- Java: jUnit
- PHP: PHPUnit, PHP Unit Testing Framework
- Python: Nose, Autotest, PyTest
Most people don’t enjoy writing tests, so if we want them to actually do it, it must be easy to:
- Add or change tests
- Understand the tests that have already been written
- Run those tests
- Understand those tests’ results
Test results must also be reliable. If a testing tool says that code is working when it’s not, or reports problems when there actually aren’t any, people will lose faith in it and stop using it.
The simplest kind of test is a unit test that checks the behavior of one component of a program. We still have to decide what to test and how many tests to run. Our best guide here is economics: we want the tests that are most likely to give us useful information that we don’t already have. Now, we should try to choose tests that are as different from each other as possible, so that we force the code we’re testing to execute in all the different ways it can - to ensure our tests have a high degree of code coverage.
Testing our code
This repository already has a test we can run that uses the jUnit unit testing framework for Java, which checks it works correctly for a single test case. In an editor, such as Nano, open src/test/java/math/FibonacciTest.java
.
$ nano src/test/java/math/FibonacciTest.java
You should see:
package math;
// Copyright 2014 The University of Edinburgh.
// ...
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import math.Fibonacci;
/**
* Test class for Fibonacci class.
*/
public class FibonacciTest
{
/** Initialise test suite - no-op. */
@Before
public void setUp()
{
}
/** Clean up test suite - no-op. */
@After
public void tearDown()
{
}
/** Test fib(1). */
@Test
public void testFib1()
{
assertEquals(1, Fibonacci.fib(1));
}
}
So we can see we have a single test that checks that an argument of 1 to Fibonacci yields an answer of 1, otherwise the test will fail. So we can use this test to verify the fix we made earlier.
Notice that there are setup()
and tearDown()
functions, which are optionally used to setup and undo any preparation required to run any of the tests, although we don’t need them here. The prefix ‘test’ for test methods and ‘Test’ for the test class is expected convention for jUnit. jUnit will automatically run any tests marked with the @Test
decorator.
Ordinarily, code should have a suite of tests which test its correct operation more completely - we’ll look into adding some more shortly.
We can build and run our test using Maven:
$ mvn test
You should see, embedded in the output:
...
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running math.FibonacciTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.077 sec
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
...
So we get a nice summary of what happened with our tests, and that the fix to the problem was successful (as reflected in the Jenkins build on the Develop branch).
Adding unit tests to our software
Given what we’ll learnt do far, let’s put it all together and write some of our own unit tests for Fibonacci.
Exercise: Write your own unit tests
Consider what other ways the code should be tested, and write 2-3 jUnit tests, adding to the
src/test/java/math/FibonacciTest
class, to check your code handles them correctly. Again, build and run your tests usingmvn test
. Whilst writing these, also create a test that deliberately fails so you can see what happens to the results.
Committing our tests
Now we should add commit our tests to our forked repository as before. We can check our changes, as before:
$ git status
On branch develop
Your branch is up-to-date with 'origin/develop'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: src/test/java/math/FibonacciTest.java
no changes added to commit (use "git add" and/or "git commit -a")
Then add and commit them:
$ git add src/test/java/math/Fibonacci.java
$ git commit src/test/java/math/Fibonacci.java
Adding in a suitable commit message.
Add test for second Fibonacci number
To more completely test the second fib() clause
Save and exit the editor, and then see this commit in our local repository log:
$ git log
commit 3b7a62d27da0876e723192be15f8b9767db09cc7
Author: bitbucket_user <bitbucket_user_email_address>
Date: Mon Sep 24 09:47:12 2018 +0100
Add test for second Fibonacci number
To more completely test the second fib() clause
commit a87f5f901c4dcc3938a2ec62a54e5a389ce2fcf1
We’ll submit a pull request - along with some other commits - later.
Adding unit tests to our software (with TDD)
Unit tests are actually such a good way to define how functions ought to behave that many programmers use a practice called test-driven development (TDD). Instead of writing code, then figuring out how to test it, these programmers:
- Write some unit tests for a function that doesn’t exist yet
- Write that function
- Modify it until it passes all of the tests
- Clean up (or refactor) the function, i.e., make it more readable or more efficient without breaking any of the tests
The mantra often used during TDD is “red, green, refactor”:
- Get a red light (i.e., some failing tests)
- Make it turn green (i.e., get something working)
- Then clean it up by refactoring
- This cycle should take anywhere from a couple of minutes to an hour or so. If it takes longer than that, the change being made is probably too large, and should be broken down into smaller (and more comprehensible) steps.
TDD’s proponents argue that it helps people produce better code for two reasons:
- It encourages them to write code in small, self-contained chunks, and to actually write tests for those chunks
- It frees them from confirmation bias: since they haven’t written their function yet, their subconscious cannot steer their testing toward proving it correct rather than finding errors.
Exercise: Implement a feature using TDD
Think of a new feature you would like to add to your code, and write the unit tests for it first. Then implement the feature in the code, and rerun the tests. Once successful, refactor your code as necessary, making it more readable and commented for other developers (including yourself) - and ensure your tests still pass after refactoring!
Question: to TDD or not to TDD?
Which approach to writing unit tests did you prefer? Using TDD or not using it, and why?
Now we should add and commit our new tests to our local repository as before (see steps above).
Push changes to fork and create Pull Request
We need to push these changes to our forked repository on Bitbucket, as we did in the previous lesson:
$ git push
Then, as before, we need to create a pull request back to the official repository via the Bitbucket interface.
Select ‘Pull requests’ from the navigation bar, and select ‘Create pull request’. What we want is to submit pull requests that will merge with the Develop branch on the official repository (since that’s what we’ve committed to). Importantly, you’ll see at the top of the page on the right, that the pull request, by default, will attempt to create a pull request for the Master branch on the official repository, so change the ‘master’ branch to ‘develop’. Also make sure that the destination repository is set to ‘cessda/cessda.ces2018.test’.
Add “WIP” to the beginning of the pull request’s title, e.g. “WIP Fix issue with incorrect Fibonacci result”. When ready, select ‘Create pull request’ at the bottom right. At some point, the maintainer will go through a list of outstanding pull requests and either merge them into the official repository or decline them.
The repository maintainer now has merged two pull requests into the official repository’s Develop branch:
- The fix for the problem
- Some new unit tests
So what the maintainer can do now, when ready (i.e. for an upcoming release), merge them into the Master branch.
Key Points
Automation benefits developers, CESSDA, and anyone that uses or develops your code.
Use CESSDA’s Jenkins CI infrastructure when developing for CESSDA.
Write unit tests for your code contributions where possible.