Automated Tests
The goal of automated testing
Developers should be able to make changes in the codebase quickly and confidently. A big part of this means knowing that new changes have not impacted other functionality in the system and everything continues to work as expected.
Of course any new functionality itself may or may not work as expected and it is up to the developer to write the appropriate tests to ensure it works correctly in both expected and unexpected scenarios. Tests should give a developer confidence in their own work, and prior tests should give future developers similar confidence.
Automation of testing should speed up development in two significant areas:
- While making changes, new automated tests can be run regularly to ensure (without lots of manual effort) that the changes continue to work as expected
- Avoid large amounts of time spent manually performing regression testing of the whole application to ensure existing functionality keeps working
Test types and expectations
We seek to have a quality codebase that developers can work on with speed. This means balancing test strategies, quantity, and coverage.
When looking at a well-factored codebase there are three common ways to automate tests (ordered by levels low to high):
- Unit tests
- Integration tests
- Backend integration tests
- Frontend integration tests
- End-to-end tests
Note: All the commands to execute the different tests can be found in package.json file.
Unit Tests
Description
Small tests of specific behavior. Each unit test is only intended to validate an isolated piece (unit) of functionality separated from the rest of the system. Any dependencies are often mocked.
Expectations
High coverage of functionality. If measured in branch coverage percentage, aim for 100%. This is the place to guarantee confidence in the system. If a higher-level test spots and error and there’s no lower-level test failing, you need to evaluate if a lower test should be written.
Execution Speed | Complexity | Fragility |
---|---|---|
Extremely fast | Extremely low | Extremely stable |
Implementation
In cht-core unit tests are located in the tests
directories of each app (e.g. in webapp/tests
you can find unit test for the webapp). Run them locally with: npm run unit
.
Integration Tests
Description
Tests to exercise how multiple components interact with each other. With a dynamic language like JavaScript these are especially important to verify expectations of interface points. These may mock some parts, but often use the “real” components since the point is to exercise those components together. As a result, these tests likely involve more setup, potentially involving data scenarios.
Expectations
Dramatically fewer than unit tests. The goal is not to verify all branches; it is to gain confidence in interface points.
Execution Speed | Complexity | Fragility |
---|---|---|
Fast execution, but slower startup when working with a DB | Mid-to-high. Things can get complex fast when combining parts! | Mostly stable. Fragility risks tend to come from DB setup. |
Implementation
For us, backend integration testing means testing through the entire stack of our application connected to other applications within our system. In the image below, it means that we test each application (box) and its interaction with other applications within our system. We isolate the tests from the webapp and make the necessary shortcuts to make the test more straightforward and faster. We do not mock any part of the system.
Backend integration tests are located in tests/integration
. Run them locally with npm run integration-all-local
and npm run integration-sentinel-local
.
flowchart LR subgraph cht-e2e [Docker: cht-e2e] api couchdb-1.local[(couchdb-1)] couchdb-2.local[(couchdb-2)] couchdb-3.local[(couchdb-3)] haproxy nginx sentinel nginx --> api sentinel --> api api --> haproxy haproxy --> couchdb-1.local & couchdb-2.local & couchdb-3.local end integration-tests{Integration Tests} integration-tests <--Pouch/HTTPS--> cht-e2e
Frontend integration tests (or web component tests) are designed to validate form behavior (including page layout) without needing to run the whole CHT. The web component isolates the enketo form functionality from the CHT webapp. This only covers forms and not other parts of the webapp. It does not trace behavior though the whole system and the database is never involved. Instead, the whole idea of the web component is to abstract the UI functionality away from the underlying backend complexity.
Frontend integration tests are located in tests/integration
. To run them locally you need to build a cht-form Web Component with npm run build-cht-form
and npm run integration-cht-form
to run the web component tests.
E2E Tests
Description
Tests that simulate real user experiences to validate the complete system. You can think of e2e test as the user main workflows when using the system.
Expectations
E2e tests give us the most confidence to decide if the feature is working, but must only check the parts of code that the lower-level tests can’t cover. We should push the testing levels as far down as possible.
Execution Speed | Complexity | Fragility |
---|---|---|
Slow. So please make sure to check existent tests and maybe just add extra assertions or minor changes instead of directly adding a specific e2e test for your new change. Also, make sure your code is performant. | Low for the test itself (click tab, enter text into form, click submit, check text on screen. Extremely high for the setup. | Painful fragility with high risk of race conditions and high maintenance burden. Please ensure your code is clean, organized, and utilizes effective selectors. |
Implementation
Our end-to-end tests are designed to test the entire system as a whole. They interact with the webapp as a user would, using WebdriverIO to control a headless browser session. They are not isolated from the rest of the system, and they do not use mocking.
End-to-end tests are located in tests/e2e
. Run them locally with the following:
npm run wdio-local
to run the tests for the default confignpm run wdio-default-mobile-local
to run the mobile tests
flowchart LR subgraph cht-e2e [Docker: cht-e2e] api couchdb-1.local[(couchdb-1)] couchdb-2.local[(couchdb-2)] couchdb-3.local[(couchdb-3)] haproxy nginx sentinel nginx --> api sentinel --> api api --> haproxy haproxy --> couchdb-1.local & couchdb-2.local & couchdb-3.local end subgraph browser webapp end e2e-tests{E2E Tests} browser <--HTTPS--> cht-e2e e2e-tests <--wdio--> browser e2e-tests o--Pouch/HTTPS--o cht-e2e
How to write automated e2e tests
Read the style guide for automated tests for guidelines on how to create new automated test cases for CHT-Core.
Debugging E2E tests
End to end (e2e) tests can be really difficult to debug - sometimes they fail seemingly at random, and sometimes they only fail on certain environments (eg: ci but not locally). This can make reproducing and reliably fixing the issue challenging, so here are some tips to help!
Set the DEBUG
flag
Setting the DEBUG
environment variable (e.g. DEBUG=true npm run wdio-local
) when running the tests locally will do the following:
- Run the browser without the
headless
flag (details in thewdio.conf
file), so the browser will be displayed when running the tests - Increase the test timeout from 2 minutes to 10 minutes
- Prevent Mocha from automatically retrying tests that fail (by default a failing test is retried 5 times, details in the
wdio.conf
file) - Prevent the
cht-e2e
Docker containers from being torn down after the test finishes
Run the e2e tests without re-building docker images
After the tests are executed the first time using the command npm run wdio-local
, the docker images are built in your local environment using the checkout branch name. If it is needed to run the tests repeatedly and there is a certainty that the cht-core code didn’t change, you can use the command npm run ci-webdriver-default
. This command will execute the e2e tests as they are run with the wdio-local
command but without re-building the images. This command uses the images that were built previously, which makes the process faster.
Read the logs
Read the failure carefully - it often has really good info but sometimes it’s just hard to find. Most importantly it tells you exactly the line in the test that failed and you can look that up in the source to see what it was trying to do. The error message itself is also really useful. Also sometimes one error causes the next, so always start with the first test failure before looking at the others.
Known failure patterns
- Can’t click on an element because another element would get the click. This usually means a modal dialog was being shown. 90% of the time this is the update notification modal which means some settings change has been detected after the test started execution.
- Stale element. This means the DOM element has been removed after it was found on the page but before trying to do something with it. Generally try to find the element just before it needs it to reduce the chance of this happening
Other logs and screenshots
GitHub actions will artifact all files in tests/logs. This is the directory any logs, results, images, etc… should save to if you want to review them if a build fails.
View the CI report
There are logs and screenshots stored in the allure reports when a job failed on the CI. To access to those logs follow these steps:
- Download the CI run artifact zip file located in the failed build’s
Archive Results
section. - Extract the
.zip
file. - From your cht-core directory, run
npx allure open <path>/allure-report/
. Being<path>
the location where the zip file was extracted.
Running just the failing test
Running e2e tests can be quite slow so to save time modify the specs
property of /tests/e2e/**/wdio.conf.js
so it only finds your test. You can also use describe.skip
and it.skip
to skip specific tests.
IntelliJ Based
- In a terminal, run
npm run build-dev-watch
- In Intellij, open the package.json file
- Scroll to the scripts section and click the ▶ button next to
wdio-local
- Select
Debug 'wdio-local'
Watching the test run
Running the tests locally (e.g. with npm run wdio-local
) will allow you to watch it run but if you interact with the page the test will fail in unexpected ways. Furthermore the browser will close after a short timeout so you won’t be able to inspect the console or DOM. To do this, force quit the process running the test before it tears down and you will be able to navigate around the app, use Chrome dev tools, and inspect the docs in the database to (hopefully) work out what’s going wrong.
Running the upgrade e2e test locally
To run the upgrade e2e tests in your local environment, follow these steps:
- Make sure your branch has been published, and it’s available in the market:
- A way to do this is by pushing the branch, let the GitHubActions to run, if all the other e2e are okay, then it will publish the branch.
- Check that your branch name is available here.
- Make sure to stop all existing containers
- Set these environment variables:
export MARKET_URL_READ=https://staging.dev.medicmobile.org
.export STAGING_SERVER=_couch/builds_4
.export BRANCH=<your branch name>
.export BASE_VERSION=<CHT base version>
(it can be usedlatest
as the value).export TAG=<CHT version>
(Optional, e.g.4.8.1
).
- Run the upgrade e2e tests:
npm run upgrade-wdio
If you experience errors such as:
Error in hook: StatusCodeError: 404 - "{\"error\":\"not_found\",\"reason\":\"Document is missing attachment\"}\n"
Try the following:
- It’s probably because it can’t find the latest released version of CHT, double check that
MARKET_URL_READ
andSTAGING_SERVER
environment variables are set.
If you experience errors such as:
If you are seeing this locally, it can mean that your internet is too slow to download all images in the allotted time.
Either run the test multiple times until you load all images, download images manually or increase this timeout.
Try the following:
- Manually downloaded the images. To download images manually, you can use either
docker compose
ordocker
:- With
docker
, you’d do a docker pullfor every image you want to download. - With
docker compose
, you’d save all docker-compose files in a folder, do adocker compose pull
, and point to your files as a source. Read more on docker compose pull
- With
If you experience errors such as:
A user session timing out while running the test.
This could be because there are some issues with data or there could be a large number of images and volumes that caused docker to crash. We recommend cleaning all the docker data to be able to start afresh and clean. It is important, however, to note that these commands will delete everything. And the e2e tests might take a little longer to run because all the images need to be downloaded again.
Try running the following commands:
docker system prune
docker image prune -a
docker volume prune -a
docker network prune
Repeat the above steps until all data has been deleted.
Test Architecture
Our GitHub actions spin up an ubuntu-22.04 machine. They install software and then launch couchdb and horticulturalist in a docker container. This is needed to run our applications in the specific node versions we support, while allowing our test code to run in versions of node it supports. This creates a paradigm to keep in mind when writing tests. Tests run on the ubuntu machine. Any test code that starts a server or runs an executable is running outside of the horti container. The ports are exposed for all our services and horti has access to the cht-core root via a volume. Horti can also talk to the host by getting the gateway of the docker network.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.