At Babbel, we employ continuous integration to detect issues early in the development process. Our CI does not only automate the build and run different test suits but also handles some tasks for documentation purposes. With so many moving parts, the setup is not trivial. We (one of the Web teams) share our TravisCI setup here on Babbel Bytes in the hope that some of you find it helpful for your own applications.
Our application and its unit tests are written in JavaScript. Additionally, we have Selenium based UI tests written in Ruby. We use TravisCI to run all our tests and build the application for deployment. After a deployment, we also need to build a so-called Storybook and upload it to AWS S3. Storybook is a UI Development Environment. Finally, we check for vulnerabilities using a service called Snyk.
This means there are five different job types (from now on called stages
).
- Run unit tests
- Run UI tests
- Build application for deployment
- Run Snyk
- Build and upload the storybook
We want these stages to be executed sequentially, and whenever a stage fails, we want the pipeline to be stopped immediately after it. This will save us time and TravisCI computing resources.
For running the UI tests, we are not using an external service provider (browser/mobile farm), but instead run the tests on TravisCI itself, and the test suite’s total runtime is quite long. For this reason, we cannot run the UI tests on every push/PR/merge, as this would delay our deployments. We decided to be bold and merge code first and run the UI tests afterwards, and roll back or deploy a fix in case an issue came up. Before we deploy to production, however, we still do manual testing in our staging environment for all code changes that require it.
Also - when running sequentially - the UI tests’ runtime is too long for a single TravisCI job. The job would time out before the UI tests have finished. So we decided to parallelize them. Instead of writing our own parallelization script, we found a nice solution offered by TravisCI itself, the TravisCI build stages. As TravisCI puts it:
“Build stages is a way to group jobs, and run jobs in each stage in parallel, but run one stage after another sequentially.”
This is exactly what we wanted. With this solution, we could run each of our five stages sequentially, and the UI tests (and potentially other jobs) in parallel.
And there is more! By enabling the TravisCI Conditions, it becomes possible to run the stages depending on GitHub information. We are using branch
, type
, and commit_message
to decide when to run which stage(s).
To keep the TravisCI runtime as short as possible, we decided to neither run the UI tests, nor build the storybook, nor use Snyk in case we
- open a PR, or
- push to any branch that is not called
master
orintegration
We do, however, run the unit tests and build the application.
When we push to master, we run all stages except the UI tests. As mentioned before, we run the UI tests after a merge because the UI test suite’s runtime is too long at the moment. Once we’ll have improved the tests’ speed, we will re-evaluate our setup.
So when do we run the UI tests? For now,
- When we push to a branch called
integration
- As part of a daily TravisCI cron job
- When we specify
run_tests
in the last commit message of a push to any branch
To tell TravisCI that one wants to use stages, they are quite simply listed (in order of execution!) in the stages
section of the travis.yml
. Each stage has a name and an optional condition. Here is our stages configuration:
stages:-name:Unit Testsif:type != cron AND branch != integration-name:Ui testsif:type = cron OR branch = integration OR commit_message =~ run_tests-name:Deploymentif:type != cron AND branch != integration-name:Snykif:branch = master AND type = push-name:StorybookS3if:branch = master AND type = push
Our standard TravisCI environment (the env
section of the travis.yml
) is configured to run the UI tests, split up using the matrix
option. The environment variables configured in env - matrix
are passed to the script
section that starts the UI tests.
script:./run_ui_tests $TEST_PARTenv:matrix:-TEST_PART=1-TEST_PART=2-TEST_PART=3
run_ui_tests
is a simple bash file that handles the setup for the UI tests:
#!/bin/bashTESTS_PART_1=("test1.feature""test2.feature""test3.feature")TESTS_PART_2=("test4.feature""test5.feature""test6.feature")# .. etcfor test_name in“${!TESTS_TO_EXECUTE}”
TESTS_TO_EXECUTE=TESTS_PART_$1[@]
do
./ui_test_run_script "$test_name.feature"done
This is how we parallelize the UI tests - all TravisCI env - matrix
jobs are run in parallel, although there can be a limit on how many parallel TravisCI instances are allowed per repository. In fact, our project is one of the larger ones at Babbel, so in order not to block other teams, we gave ourselves a limit. This can be configured under https://travis-ci.com/YOUR_COMPANY/YOUR_REPO/settings:
We are also auto canceling jobs in case we push to a branch on which a job is currently running:
Now that we have env
set but don’t want to run the UI tests in the other stages, we need to overwrite it with either another or no environment variables. This is done in the jobs - include
section of the travis.yml
.
jobs:include:-stage:StorybookS3env:# OVERWRITE WITH EMPTYinstall:install_script_for_storybookscript:bundle exec s3_website push-stage:Unit testsenv:TARGET=target_environmentinstall:install_script_for_unit_testsscript:run_script_for_unit_testsafter_success:upload_coverage-stage:some other stage..
Here are the stages related parts of our .travis.yml
, from top to bottom:
install:-# install whatever is necessary to run the UI testsscript:.run_ui_tests $TEST_PARTenv:matrix:-TEST_PART=1-TEST_PART=2-[...]# the following line is needed to enable the TravisCI build conditionsconditions:v1stages:-name:Unit Testsif:type != cron AND branch != integration-name:Ui Testsif:type = cron OR branch = integration OR commit_message =~ run_tests-name:Deployif:type != cron AND branch != integration-name:Snykif:branch = master AND type = push-name:StorybookS3if:branch = master AND type = pushjobs:include:-stage:Unit Testsenv:TARGET=target_environmentinstall:./install_unit_tests_dependenciesscript:./run_unit_testsafter_success:./upload_coverage-stage:StorybookS3env:# no environment variables - overwrite with emptyinstall:./install_storybook_dependenciesscript:./build_storybookafter_success:./upload_storybook-stage:Deployname:Deploy to stagingenv:ENVIRONMENT=staginginstall:./install_deployment_dependenciesscript:./deploy-stage:Deployname:Deploy to productionenv:ENVIRONMENT=productioninstall:./install_deployment_dependenciesscript:./deploy-stage:Snykenv:# no environment variables - overwrite with emptyinstall:./install_snyk_dependenciesscript:./run_snyk
A full run on TravisCI for the master branch, containing all build stages, looks like this:
Alternative approach for parallelizing
We realize that there is another way of parallelizing the UI tests; it can be done using only stages
without env - matrix
:
jobs:include:-stage:UI testsname:part1install:.install_ui_test_dependenciesscript:.run_ui_tests 1-stage:UI testsname:part2install:.install_ui_test_dependenciesscript:.run_ui_tests 2
We did, however, feel that this solution made the travis.yml
file longer and harder to read. TravisCI build stages are still a beta feature, and matrix
will probably be enabled for them sooner or later, and then we will most likely change our approach.
To sum up, in this article you have been introduced to the challenges of setting up continuous integration for a medium-sized project. We showed you how to group jobs in stages, run tasks within one stage in parallel, and run stages conditionally. We hope you find our ideas useful and are looking forward to hearing your thoughts!
The author would like to thank Annalise Nurme for providing the drawing of the juggler.