In this tutorial, we'll review why Continuous Integration is indispensable to the development lifecycle of both solitary developers and teams and how we can immediately begin to reap its benefits.
Continuous Integration (CI) is a software development practice wherein developers regularly merge their code with a central repository after which automated builds and tests are run.
Principles of Continuous Integration
Before we get our hands dirty with a demonstration, let's briefly discuss the principles of Continuous Integration.
Maintain a single source code repository
Continuous Integration (CI) advocates the use of a version control system to track changes in code.
Everything needed for a fully functional application should be pushed to a single repository.
It should be trivial to trigger a complete rendering of the project with a single command. This should include tasks like the creation and migration of databases and configuring project environment variables.
Make the build self testing
The CI tool used should trigger a run of the project tests after the build is complete. Of course, this necessitates that comprehensive tests be written for any work to be integrated.
Before the code that triggered the build is merged with the main branch, all tests must be seen to pass.
Make frequent commits to the main branch
Making atomic and frequent commits encourages integration and makes conflicts between work from different developers easier to manage and resolve.
After a section of work has been built and passes tests on the CI server, it should be merged to the main branch after review.
Every commit should trigger a build on the CI server
This assures that each commit made has not broken the build. It also becomes trivial to pinpoint a commit that caused errors in the build.
Broken builds should be fixed immediately
When it is discovered that a commit has caused a build on the CI server to fail, the commit should be analysed and the cause of the break resolved as soon as possible.
The CI build should be done in a replica of the production environment
Running the build in an environment with little or no deviation from the production environment assures that running the same application in the actual production environment will not be fraught with unwelcome surprises.
The output of CI builds should be visible
Build status should be made available to all relevant stakeholders. Circle CI can be configured to send email and web notifications after a build has completed.
Advantages of continuous integration
The benefits of implementing Continuous Integration in your development cycle are several. Let's note down a few here.
- Since build output is extremely visible, when a build fails, we can find and resolve bugs quickly.
- Continuous Integration helps to enforce testing in our applications, since we rely on these tests to assess the success of our builds.
- Because the build is run in a production like environment, CI reduces the time it takes to validate the quality of software to be deployed.
- Since atomic commits are encouraged, CI allows us to reduce integration problems with existing code, enabling us to deliver better software faster.
CI for solo developers and teams
Now that I've bended your ear with praises for Continuous Integration, let's sober up a little.
It's evident how the benefits we discussed above would be extremely valuable for teams. Now, let's focus on how CI can enrich development for solo developers and whether the benefits outweigh the costs.
All the advantages we mentioned above still apply to solo developers but if you're solo, you should think about the following before implementing CI.
- CI takes time: Depending on the complexity of your application's environment, it may require a lot of manpower to set up your CI environment in a way that mirrors your actual production environment. If your production environment changes, you'll need to reconfigure your CI environment too.
- Tests take time: Again, depending on the complexity of your application and the thoroughness of the tests running in the CI environment, your tests may take a while to run and confirm the health of the build. Since the build and tests should ideally run on every commit made, this could be an expensive operation. You may also need to spend valuable time optimising how your tests run. If you want to move fast and break things, this may be a little frustrating.
- CI is a way of life: Continous Integration begins and ends with the developer. It's a commitment not to be taken lightly. When you don't have a team that reminds you of the value of the process, it can be a lonely and tiring path to walk.
- CI can be a red herring: Developers implementing CI have to ensure that they are not lulled into a false sense of security by passing builds. This is even more important when you're working alone and without the benefit of other processes that could alert you to unseen problems. The build is only as good as the tests that run in it.
Now that we've got a handle on what Continuous Integration is, let's see what it entails. We'll need accounts with the following services, so go ahead and register.
- Github: We'll use Github to host our git repository.
- Circle CI: Registering on Circle CI with your Github account will make things easier in future. This has the advantage of adding CircleCI integration to your Github account.
- Coveralls: Samsies here. Sign up with Coveralls using your Github account.
Before we start, you'll need to install a few applications globally.
- Python: I'll be using v3.5.2
- virtualenv: A recommended installation to isolate our application environment.
The following will be the scaffolding for our project, so go ahead and create these directories.
+-- python-ci +-- src | +-- math.py +-- test | +-- math-test.py +-- requirements.txt
Next, copy and paste the following into the
requirements.txt file at the root of the project.
appdirs==1.4.0 astroid==1.4.9 click==6.7 coverage==4.3.4 coveralls==1.1 docopt==0.6.2 isort==4.2.5 lazy-object-proxy==1.2.2 mccabe==0.6.1 nose==1.3.7 packaging==16.8 pyparsing==2.1.10 requests==2.13.0 six==1.10.0 wrapt==1.10.8
Finally, create and activate the virtual environment for this project then run
pip install -r requirements.txt
This will install all the required dependencies for our project in your current virtual environment.
Let's create a simple class with a function that returns the sum of two numbers. Add this into your src/math.py file.
class Math(): def addition(value1, value2): if not isinstance(value1, int) and not isinstance(value2, int): return 'Invalid input' else: return value1 + value2
Next, let's write a test to make sure our function is working as expected.
import unittest from src.math import Math class MathTest(unittest.TestCase): def test_addition(self): # Make test fail self.assertEqual(Math.addition(3, 4), 8)
You'll notice that we've made an incorrect assertion in the test. Let's let it slide for now.
Now, run the following in your shell to make sure the test fails.
Expect similar results
F ====================================================================== FAIL: test_addition (math-test.MathTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/emabishi/Desktop/Projects/Personal/python-ci/tests/math-test.py", line 8, in test_addition self.assertEqual(Math.addition(3, 4), 8) AssertionError: 7 != 8 ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)
Let's create a repository on Github that will hold our application.
Once you've signed up, create one. I'll be calling mine
Next, it's time to initialise our local project directory as a git repository and add a reference to our github remote. At the root of your project, run the following commands:
git init && git remote add origin <enter-your-github-repository-url-here>
A second option would be to clone the Github respository to our local machine. We can do this with a single command.
git clone <enter-your-github-repository-url-here>
Let's create a new branch called
develop and check out to it with the command
git checkout -b develop
After this, we can add and commit our previous changes using
git add . && git commit -m "<enter-short-commit-message-here>"
Whew, we're done. I'd pat you on the back if I could. I promise you that the hardest part's over.
Circle CI is a service that helps us employ Continuous Integration by letting us build our application and run tests in a configurable environment.
It's also pretty handy because it can send us email and web notifications of the status of our current build. You can also integrate it with messaging services like Slack for real time notifications.
We just created an account with Circle CI, so let's take full advantage of it. Log in to your Circle CI account using Github authentication.
Once logged in, you'll be directed to your dashboard. Click on the fourth icon on the task bar on the left side of the screen. This is the projects tab and lists all your current Github repositories.
You'll see a page like the one below.
Click on the build project button next to the name of the repository you created before. Sit back, relax and watch what happens.
Circle CI runs our build but eventually errors out. Helpfully, it provides us with an error message.
To configure settings for the build on Circle CI, we'll need to create a configuration file in the form of a file with a .yml extension. Let's create a circle.yml file at the root of our project.
Fill it with the following:
machine: python: version: 3.5.2 dependencies: override: - pip install -r requirements.txt test: override: - nosetests tests/math-test.py
The machine section configures the virtual machine we're using. Here we're explicitly defining that the machine should run python version 3.5.2.
We use the dependencies section to install our application prerequisites and the test section to specify the command which will trigger our tests.
As you can see, it's exactly the command we used to run our tests locally.
As expected, with much pomp and circumstance, our tests have failed and failed loudly. We'll remedy this soon enough. Leave them in their imperfect state for now.
To ascertain the extent to which the tests we've written cover our code, we'll use Coverage.py. We've already installed it so there's little we have to do now.
At the root of our project, run
coverage run src/*.py && coverage report
Expect similar results
Name Stmts Miss Cover ---------------------------------------- tests/math-test.py 5 3 40%
You can tweak Coverage.py reporting in all sorts of interesting ways. If you're interested, have a look here. For our purposes, the default reporting will do for now.
The next service we're going to take advantage of is Coveralls. Coveralls displays our test coverage for all to see, thus making efforts at Continuous Integration loud and visible.
Once you sign in, you'll be directed to your dashboard.
Click on the second icon on the task bar on the left side of the screen. This is the add repo tab and lists all your Github repositories that are currently synced to Coveralls.
To refresh the list of repositories under your account, click on the Options menu on the right of the page linked to your Github account
Next, search of the name of the repository we recently created. In this case, I'm looking for
python-ci. Your repository name may be different.
Click on the switch to turn it to its ON position.
Click the details button next to the name of the repository. You'll see something like this:
Take note of the repo token.
To register our CI build with the Coveralls service, we have to perform some configuration. Let's do this by setting our Coveralls repository token as an environment variable on Circle CI.
Click on the gear button on the top right of the Circle CI build page. Follow along by clicking the Environment Variables tab on the left of the page. Finally, click the Add variable button and add the token like this.
Now, let's edit our circle.yml file to send coverage data to the Coveralls service after our tests run. Edit the circle.yml to look like this.
machine: python: version: 3.5.2 dependencies: override: - pip install -r requirements.txt test: override: - nosetests tests/math-test.py post: - coverage run src/*.py - coverage report - coveralls
Add and push the changes to your remote Github repository. The push will trigger another build on Circle CI.
If we go back to our Coveralls dashboard, we'll notice our dashboard now displays our test coverage percentage. We can even tell at a glance how our coverage has changed over time.
Sometimes it takes a while for the data to reflect, so give it a few minutes.
Github Status Checks
Going even further, in the spirit of Continuous Integration, we can prevent pushes or merges to the main branch of a repository until all required checks pass. In our case, these checks will be our tests.
We'll implement this using a helpful tool by Github called Status Checks.
Let's navigate to the settings tab of the repository we created on Github. Under the settings tab, select the Branches menu.
On the protected branches menu, choose to protect the master branch.
From there, make the following selections and save your changes. You'll be prompted to enter your password to authorise the changes.
To see the power of Github's Status Checks in action, let's make a Pull Request comparing the master branch of our repository with the develop branch.
As can be seen below, our tests failed on Circle CI. Therefore, because of the checks we put in place, merging to master is blocked.
Let's fix our tests, make a push to Github and watch our Pull Request for any changes.
Wohoo! Green all the way. We can now merge our work to the master branch with assurances that all our Continuous Integration checks have passed.
We've demonstrated that employing a Continuous Integration strategy in software development leads to major benefits in the quality of our work and the rapidity at which we resolve issues and conflicts within our application code.
Even more of interest is that when paired with Continuous Deployment strategies, CI becomes an even more powerful and capable tool. We've not gone into leveraging Continuous Deployment in our workflow, but I trust that you'll look into the possibilities that its use opens up.
However, there's a lot more we can do with CI which we've not gone into here. If you're interested, I'm leaving some links to a few resources which I believe will prove extremely helpful.
If you'd like to read more about Continous Integration, here are a few places you can start with.
A few other 3rd party CI tools are:
This list is by no means exhaustive.
I'd love some feedback on this guide. If you have a moment, drop me a comment in the box below. Also, if you have any questions, don't be shy, let me know.