Continuous integration basics
Continuous integration is the practice of building and testing a codebase for every proposed change, often executing the process multiple times per day across a software development team. Each developer integrates their changes into the official latest version of the codebase to identify integration issues early in the development process. Additionally, the execution of the full test suite provides broad regression protection even as developers work on isolated areas of the codebase.
The automated tests are usually unit and integration tests, but can include end-to-end tests as well. Because the tests are automated, they are expected to run quickly and provide feedback about the validity of the proposed change.
The adoption of continuous integration has grown in recent decades, and the practice falls under the broader umbrella of DevOps, which aims to codify configurations, shorten the development cycle and provide rapid release of quality software. DevOps itself stems from the early days of Agile software development, and it’s in relation to both concepts that this article discusses continuous integration, and modern practices in end-to-end testing. For more information on CI/CD in Agile, see our other article on End-to-end Testing in Continuous Delivery for more information about these two concepts that are often implemented together.
Continuous integration in Agile: where does it fit?
The Agile methodology encompasses many principles, but the most relevant aspects for continuous integration are the focus on quick iteration and quality. Agile breaks a large software project into much smaller increments, and then implements a development cycle of: plan, build, integrate and review. Agile favors delivering working software quickly and then “standing on the shoulders” of that software to build the next component, and so on.
Continuous integration provides the means for ensuring the quality of the software as it grows. For each new feature or component that is developed, it is automatically integrated into the main software codebase, and the full suite of automated tests executed against the change. In this way, bugs are caught immediately, before other code has a chance to depend on it.
This also highlights the importance of having tests for newly-written software. While the automated tests in continuous integration will detect and prevent introducing regressions for code that is already working, it does imply that the new code change includes with it tests to confirm its own validity. These new tests join the test suite and serve as regression prevention for future changes.
Test-driven development in continuous integration
Test-driven development (TDD) is a concept from Extreme Programming (XP), which is itself a framework for implementing Agile. Test-driven development is the practice of writing coded test cases first, as derived from software requirements, and then writing the software such that every test case passes. Only once the test cases are all passing does the developer then go back and refactor their code to optimize for performance requirements. The test cases are often unit tests, which are meant to test a specific, narrow behavior of the software and should run on the order of milliseconds.
While TDD is not a requirement for continuous integration, it supports the ideals of continuous integration perfectly in that each new feature or change carries with it tests explicitly proving its validity. Furthermore, the tests serve as examples for using (or calling) the new code. Test-driven development in continuous integration effectively layers software requirements into each software build.
To the extent that test-driven development enhances or speeds up the development process, continuous integration ensures that future developers reap a second benefit from the tests during their own development process.
Continuous integration versioning and branching strategy
With each developer’s code changes executing a full build and test pass, you need to decide how to handle the multiple versions of the codebase, or branches in common version control systems, such as Git. Your continuous integration versioning and branching strategy will often involve allocating a new environment for each build execution. This environment could be as lightweight as a separate directory on a shared machine, or as heavyweight as a newly-launched virtual machine on a remote server.
Most of the time, developers will first execute a build locally on their own machine. This build might be a full build of the entire codebase, or it might be a build and test pass of only the smaller section of the codebase that their changes modify. Since developers know that their proposed change will undergo a full build and test pass as part of the continuous integration system, they don’t necessarily have to run a full build and test pass locally.
Developers share their code changes with the team and the updated version of the codebase is reviewed by the team while simultaneously being run through the continuous integration build. The team manually inspects the code and provides feedback or requested changes, which the developer can address and then re-initiate the continuous integration build. Once the change receives approval from the team and the integration build succeeds, it’s merged into the codebase and potentially released publicly as part of continuous delivery, or continuous deployment. As long as the build and tests are fast, the feedback loop is tight and inspires confidence in the change.
Continuous integration for end-to-end tests
To this point, we’ve mostly alluded to unit tests as being part of the continuous integration build. But what about end-to-end tests for webapps and other UI-based applications? Can such tests be executed as part of an automated build? Historically, end-to-end tests have been a manual process conducted against a release candidate. They were tedious to perform and, as a result, were left to the end of the software development cycle.
One of the challenges with end-to-end testing is the requirement that the application be running in a live environment that is accessible to the tester. Many modern continuous integration systems also support deploying (or delivering) the current version of the software to an environment for the purposes of testing. However, even if the software is quickly deployed, human-in-the-loop manual testing against the environment is severely limiting. Any manual process is comparatively slower and error-prone.
Selenium and similar libraries came onto the scene over a decade ago to change that, but brought with it a new set of challenges. Code-based tests seem to meet the continuous integration bar for rapid execution, but carry a maintenance cost since they are much more time consuming to author than unit tests.
Reflect takes a new approach to end-to-end testing that derives principles more akin to unit tests: fast to create and fast to run. Like all end-to-end testing, it requires that the software be deployed and available, but modern continuous integration implementations often support this. See End-to-end Testing in Continuous Delivery for more information about testing your code end-to-end in staging environments.
Implementing continuous integration in agile development
Just as the concepts of agile and continuous integration have evolved over the years, so have the implementations of these concepts in modern software development. Best practices have emerged, but you’ll often find software teams implementing continuous integration in a custom way that suits their delivery environment best. Beyond simply building the code, most modern tools can automatically provision infrastructure, expose customizable interfaces for collaboration, send notifications for broken builds and even report on test coverage. Here are some examples of how continuous integration can be implemented today.
1. GitHub Actions
GitHub Actions is a feature within the larger GitHub platform for maintaining an organization’s codebase and deploying software artifacts. GitHub Actions execute workflows based on events and triggers, and have access to multiple different versions of the codebase. This makes it possible to build and test different versions of the codebase simultaneously. Furthermore, the deep integration with GitHub itself makes for a seamless development experience.
They use a structured YAML file for defining the workflow’s behavior, and also provide support for historical builds and syntax highlighting. A major benefit of this approach is that the workflow itself is checked-in to the repository along with the code. Lastly, there is a broad and growing list of third-party workflows and packages that you can use to compose your continuous integration build quickly and reliably.
CircleCI is a hosted service for CI servers that builds and optionally deploys your codebase on-demand and through asynchronous triggers. CircleCI integrates with your source code provider (or your own Git server, for example) and responds to code changes in the repository by automatically building and testing it. With the rise in container-based services and deployments, CircleCI supports composing and executing containerized tasks.
Being a full-fledged SaaS platform for continuous integration (and continuous delivery), CircleCI has many additional features focusing on:
- staging versus production environments and user management,
- parallelism, execution and analytics, and
- automated deployment (continuous delivery) and rollbacks.
These robust features make a favorite choice among large software organizations with complex and custom requirements.
Jenkins is one of the leading open source integration servers.
Jenkins uses the concept of “pipelines” to describe the automated process
of building your software artifacts and, optionally, deploying them to production.
Continuous integration pipelines are defined in a
which is a text file that defines “stages” of the build and is checked-in to source control.
Each CI build executes on the server and posts results to a self-hosted webpage,
which includes other pages such as an administrative panel, etc.
Jenkins is open source which means you have the flexibility to run the server on any machine you want,
but also means that you need to perform maintenance of the server and the machine.
It also has a large ecosystem of plugins and supports defining your own,
which makes it the most flexible option of these CI tools.
The Agile and DevOps movements champion rapid iteration and automated procedures throughout the software development lifecycle. Continuous integration supports that mission by rapidly discovering cross-functional issues and ensuring software quality. Initially comprising mostly unit tests, modern continuous integration pipelines can now include automated end-to-end tests to verify real end-user experiences. These tests demand a flexible and dynamic platform such as Reflect to deliver on-demand comprehensive testing. Reflect supports numerous continuous integration platforms, as well as any custom-engineered build pipeline through its lightweight API.