Continuous Integration (CI)
[Article 3 of 6 in series Software Delivery, DevOps and CICD for the uninitiated]
In this article, we will cover:
- the key steps involved in creating quality software
- the practices and types of tools that are used to accelerate these steps, focusing on their roles rather than specific brands (think ‘Source Code Repository’ over ‘GitHub’, ‘Automation Server’ over ‘Jenkins’), leaving me to select which tool suits me.
- a high level software architecture and process flow that achieves ‘Continuous Integration’.
A key component of the DevOps movement has been the emergence of a software delivery paradigm by the name of ‘CICD’; standing for:
- Continuous Integration (CI)
- Continuous Delivery (CD)
Saving ‘CD’ for a following article, let’s look once more to Amazon for their definition of Continuous Integration:
Continuous integration is a software development practice where developers regularly merge their code changes into a central repository, after which automated builds and tests are run. The key goals of continuous integration are to find and address bugs quicker, improve software quality, and reduce the time it takes to validate and release new software updates.
Ok, so it’s a start, but unfortunately we are again left wondering what this actually means in practice. Further internet searches were just as disappointing, throwing up only a combination of other vague descriptions or boasting, and very low-level instructions aimed at developers, ops and software architects on how to install and configure specific tools which were apparently relevant.
But none of these tackle the crux of the matter: what would a Continuous Integration process look like if I were to introduce it to my organisation or personal project?
Whether its a website, a mobile app, or a batch file processor, all software applications are built from many (perhaps millions of) lines of code strictly organised into functions and modules performing specific tasks and working together precisely to achieve the desired end result. One single awry character could cause the whole application to fail.
Every time a new feature needs to be added, or a reported bug fixed, the software’s underlying code needs to be changed and there is a risk of breaking the application. As such, when making changes software engineers take great care to always leave a version of the application that is known to be working, and do their development on an isolated copy (a ‘branch’) of the code, only merging them back in to the core (‘trunk’) version once they have proven their change to be successful on their own machine and it does not break anything that was previously working. The essence of ‘Continuous Integration’ (i.e. the ‘Integration’ part) refers to precisely this practice of merging changes to leave the product in a working state; but there’s a lot more to it then that!
The first practice of Continuous Integration is purely process-based; nothing to do with tools: branches should be as small as possible and merged back into the main code as frequently as possible. In this way it is easy to identify the causes of issues found and rollbacks are small.
Even in the scenario of a single small project on a single computer on a single site, managing branching and merging without assistance would be horrendous. Now add in multiple developers, multiple computers and sites, simultaneous changes etc. and its easy to see how quickly this becomes simply impossible. Introducing our first team member:
A. Source Code Management Tool
What does it do?:
- stores source code
- allows developers access to copy the current version to their own computers (incl. permission management, remote access across different offices)
- allows developers to create ‘branches’ of the code, where they can work on changes without affecting the core version
- gives tools for colleagues to review a developers changes in a branch and to comment on or approve them
- allows changes to be merged back in to the original code, including tools for identifying and resolving conflicts where two simultaneous changes have made differing change to the same code
- tracks all changes made to the codebase
Examples: SVN, Git
Nb. Git is arguably the leading SCM tool around, but its power is not unlocked without remote (typically web-based) Git repositories, e.g. GitHub, GitLab
The changes developers ‘commit’ need to then get ‘built’. As described in Software Delivery 101 (Environments and Applications), this should take place on our Build server, which is common to all the developers in our project.
Building a software application from it’s source code could involve a user following a long list of manual steps (‘download X, put X in directory Y, run Z,… etc.). But since we’re interested in maintaining consistency and saving time, so perhaps we could do this automatically?
Introducing our next team member:
B. Build Tool
What does it do?: Orchestrates the build process for your application, whether this involves compilation, pulling of dependent binary files, packaging etc.
Examples: Gradle, Maven, Ant
The precise build tool for your application will depend heavily on the type of application you are building.
Whatever your software application is, it is highly likely to depend on other files which is not source code you are developing. For example, it could be:
- Third party software libraries (Software 101: don’t re-invent the wheel! if someone’s already written something doing what you need, re-use it!)
- Other ‘binary files’ e.g. the image of your company logo for the loading screen of your mobile app.
When the software is being built, it is necessary for the developer to make sure that the right version of the right file is named the right way and placed in the precise location, else the software will not run. Managing these dependencies can be a major headache for a development team, who want to focus on the code they are writing. How do we remove that headache? You guessed it! There’s a tool to help:
C. Binary Repository Manager
What does it do?: Manages files (e.g. libraries, images, code builds, etc.) in a defined directory structure so that they are centrally available to build and deployment tools. The build tools can then be configured to look for dependencies in a consistent place.
Also serves as the host for built application versions: more on this later.
Examples: J-Frog Artifactory, Nexus
So now our developers are not only assisted in writing their code, but they can also build their application at the click of a button, including automatic management of their dependencies. But the journey doesn’t stop there: next they need to assure themselves that the software they have written and built both runs and does what it’s supposed to do. They need to test the software.
The types of test needed for each software project will vary from case to case. We will revisit testing in the next article (see foot of page), but for now we will focus our attentions on the following types of test, which are an essential part of any software project:
The smallest testable aspect of a software project’s code are the basic modules/code files out of which it is comprised. Unit tests ensure each of these are working as expected when in isolation of each other.
Unit tests run directly against individual units of code without the need for a wider environment. Any external resources to which the classes and functions normally require (e.g. a database, a task queue etc.) are instead “mocked” or “stubbed” with fake versions. As such:
- A failing test always indicates that the code in that unit is not doing what it should.
- Succeeding test sets do little to confirm that the application as a whole performs as expected.
Unit tests can be run with minimal setup, cheaply and quickly, providing almost instant feedback.
Make sure that components/modules of code that comprise an application work properly with one another. The next level up from unit tests.
Integration tests depend on an environment of interconnected software and services which should be as reflective as possible of the environment in which the application under development will eventually be deployed. For example, if an application uses a database to read and write data, the integration environment should have that same database with some sane test data available. As such:
- A failing integration test could either be down to code, or down to environmental issues.
- Successful integration test sets gives confidence to the business that the overall application behaves as it should.
Unlike unit tests, integration tests are more ‘expensive’. They may require complex setup in order to create the right environment, and can take many hours to run since the tests flow through the entire application.
NB. There is a myriad of terms you might encounter describing different types or levels of software testing and it can be confusing telling the difference. The key is that any two testing terms are not necessarily mutually exclusive (e.g. whilst ‘Functional Testing’ and ‘Non-Functional Testing’ tackle entirely different types of test, ‘Smoke Testing’ may include both ‘Functional’ tests and ‘Non Functional’ tests.). The following diagram helps by excellently organising contrasting test types into groups (read their definitions here).
Any and all types of test can be conducted by hand, by diligently following a test script. The problem with manual testing is that it is resource heavy, painfully slow, and also prone to error or omission. This means we tie up skilled people for long periods of time and have slow (and inaccurate) feedback loops. A developer might wait days or weeks to learn that they need to make a tweak, meaning if the developer’s time is an investment, then the return on investment (the business value in making and realising the change) is delayed. We need to speed things up:
D. Automated Test Execution Engine(s) for Unit and Integration Tests
What does it do?: Sets up and executes automated tests against a software application, assessing and reporting on their results against expected behaviour. Automated tests provide feedback to developers within minutes, to accelerate delivery cycles and realise returns on investment sooner.
In contrast to higher level testing types (see Continuous Delivery), Unit testing and Integration testing are very dependent on the language the application is written it, so there is usually one or more tools to choose from based on your programming language (some languages even have built in support for this type of testing).
Examples: JUnit, PyUnit, Karma
Tests of any nature are established by defining test scenarios, providing test inputs and expected outcomes. It is important that the whole development team are involved creating and maintaining automated tests, not just specialist QA engineers.
In fact, the best-practice of Test Driven Development (TDD) puts the writing of unit tests as not only an integral part of the development process, but the first step, before any code changes are made. Applications built to be testable are more successful. Similarly, Behaviour Driven Design (BDD) advocates that requirements are expressed in terms of the tests that should be expected to pass afterwards, in the form ‘Given x is true, When y happens, Then z is the outcome’. This not only makes writing the automated tests easier afterwards, but also removes ambiguity from the requirements for developers.
When should Unit and Integration tests be run?
Because we can’t afford for developers to sit around and wait for an integration test set to complete with each change they make, we differentiate when these tests are run:
Unit tests should be run:
- by developers regularly during their development process
- by developers immediately before a code commit
- as part of the build process for every build, being upon, or soon after every commit.
Integration tests should be run:
- by developers before a major commit involving many components or side effects
- as part of a periodic ‘integration’ build e.g. every 24 hours.
- as part of builds that will be released
Nb. It is not just the new tests or those which have been updated as part of a code change which are run — it is the entire set of tests. This is to ensure that no ‘regression’ (i.e. unintentional degradations to existing functionality) has been introduced. Developing automated tests is essential in any modern-day software project because it is the only realistic way to check and recheck your development. Some outsiders to the development process may not appreciate that writing and maintaining automated tests can take as must time and resource on an ongoing basis as actually developing the software, if not more!
A key goal of Continuous Integration is to maintain constant releasability of the product. This means that test results should be taken seriously — investigation and resolution of any failures should be a top priority for the development team.
So with that, our developers are now able to make changes to the code easily, build their new version of the application, and have it automatically tested, giving them peace of mind that they have met the requirements and haven’t broken anything! We have one more trick up our sleeve: it’s the developer’s equivalent of that handy ‘Grammar checker’ you run the night before handing in your 200 page dissertation…
E. Code Quality Manager
What does it do?: Assesses code for resource leaks, null pointer references, security vulnerabilities, adherence to organisational coding standards and test coverage.
Nb. A common metric for software applications is ‘code-coverage’, meaning the percentage of lines of an application’s source code that are reached by at least one unit test. Code Quality Management tools can even be configured to fail builds that do not have sufficiently high code coverage!
At this point, our developer has a whole host of tools to help them consistently build high quality software. But wait, there’s even more they can squeeze out of the their existing tool set. Once the tools described are used to verify that a software build meets the quality expected, the build can be stored as a new version of the application within the Binary Repository Tool. It will reside there so that application testers, end users, or (next-article-spoiler-alert!) any deployment processes can obtain it…
So we’ve now met almost all the players in the team; all except one — the head honcho, without which our Continuous Integration dream falls apart. Introducing, El Capitán…
F. Automation Server
(also known as a Continuous Integration Server or Build Server)
What does it do?: Provides the framework to trigger and orchestrate the tools which build, test and inspect committed code, report on the results and store output.
Examples: Jenkins, Hudson
Jobs in the Automation Server can be kicked off in the following ways:
- Notification received from an external system. E.g. Automation servers support integrations with Source Code repositories so that they can be notified of any changes to the code.
- Scheduled (e.g. every 15 minutes, or daily)
- Manual launch by a user
The Automation Server’s home is the build environment.
If the Automation Server is the Conductor of the orchestra that is made up of all the other tools we have discussed, what does this Orchestra’s symphony sound like when we pull it all together?
For the Main Build (indicated as A in the above diagram)
1i. A developer commits an application code change to the Source Code Management repository
1ii. The SCM tool is configured so that the commit results in a notification being sent to the Automation Server in the Build environment. The notification causes the Automation Server to:
2. Download (‘pull’) the complete Application from the Source Code Management tool into the Build environment, and:
3. ...kicks off the build of the application including…
4. … pulling all dependencies from the Binary Repository; and…
5. … runs all the Unit Tests against the application’s individual components; and…
6. …assesses the Code’s Quality against established quality gates: and…
7. If any of the steps 3 through 7 are unsuccessful or unsatisfactory, an notification is sent to a developer mailing group so that they can inspect and address the reason for the failure. Fixing failed tests should be more important than any other activity so as to ensure that the application is always left in a releasable state.
Assuming though that all steps are successful, the built application is uploaded (‘pushed’) to the binary repository as the latest ‘main build’ version (with an incremented version number) of the software application.
8. The Automation Server presents the precise results of the latest build, and the history of all recent builds in a reporting screen.
On a nightly basis, a scheduled QA build (indicated as B in the above diagram) executes following a similar process. Failures are again reported so that they can be fixed, but if everything runs successfully, the build is pushed to the binary repository as the latest ‘integration build’ which can be deployed to subsequent environments in a Continuous Delivery process.
In this article we explored how we can accelerate the creation of quality software by:
- covering 6 types of tool, providing Open Source examples for each:
A. Source Code Management Tool
B. Build Tool
C. Binary Repository Manager
D. Test Execution Engine(s) for Unit Testing and Integration Testing
E. Code Quality Manager
F. Automation Server
- describing in a high level software architecture how these tools can be strung together via to create a Continuous Integration process flow
Next Article in the Series
- Testing Types
- The correct way to use integration tests in your build process
- Delivery Pipelines, with Jenkins 2, SonarQube, and Artifactory
- Test automation and Continuous Integration & Deployment (CI-CD)
- Traditional Development/Integration/Staging/Production Practice for Software Development
- Using the Git client
- Understand Git branching strategies
- Best automated unit testing told and their features