Using the Network Shim
Table of contents
About
The Network Shim makes it possible to record and play back network traffic during a cypress tests run. This means no live network requests need to be made when running e2e tests on CI, which is generally considered an anti-pattern as it can lead to flaky tests.
It consists of a cypress plugin as well as a setup utility to be placed in cypress' support file. These have to be used in conjunction. Under the hood it uses the cypress cy.intercept()
API to intercept network traffic during a test run. It has three different modes:
capture
: when operating in this mode, the Network Shim will allow all outgoing network traffic to the specified hosts and and store each response as a fixture (JSON file).stub
: in this mode the Network Shim intercepts all outgoing requests to the specified hosts. It will reply with the responses from previously recorded fixtures.live
: the Network Shim does not interfere with any network traffic when this mode is enabled. All network traffic is live.
By recording fixtures during a capture run and then using these fixtures during a stub run a full cypress test suite can be executed without a live backend. This is especially useful when running cypress tests on CI. Since it is no longer necessary to have a network connection and a backend, the cypress tests on CI can be more stable. One caveat is that the tests are slightly less realistic, since the network traffic is mocked, so the developer will have to make sure that the recorded fixtures are kept up-to-date.
A capture or stub run is executed for a specific DHIS2 api version and individual fixtures for each version are recorded/played-back during a run. This makes it easier to do cross-version testing for feature-toggling apps.
Quick setup
The command-line-tool in the package @dhis2/cli-utils-cypress
comes with an install
command which generates most of the configuration files documented below. See the Setup Cypress for your project guide for more info on how to do this.
To get up-and-running quickly do the following:
-
Follow the getting started guide to install and configure cypress
-
Add user credentials to a
<root>/cypress.env.json
:{
"dhis2Username": "USERNAME", // secret
"dhis2Password": "PASSWORD", // secret
"dhis2BaseUrl": "DHIS2_BASE_URL" // machine specific
}Ensure to git-ignore the
./cypress.env.json
file
-
Install tools needed to run the scripts in
package.json
:yarn install -D start-server-and-test
-
Create scripts
package.json
as demonstrated here
-
Perform a capture run to generate fixtures and commit the generated files:
yarn cy:capture
-
Copy-paste GitHub Actions job from here into the app's workflow to enable running e2e tests on CI
General workflow
Once the Network Shim is up-and-running, the general steps required get a cypress test-suite running on CI are as follows:
- Ensure a live backend is up and running
- Ensure a fully passing cypress test-suite is in place
- Ensure the CI workflow includes a job that runs the cypress test suite in stub mode
- Execute a capture run, which will produce fixture files
- Commit the generated fixture files and push to the remote
- The test suite will now execute on CI
To setup cross-version testing essentially means taking some of the steps above multiple times:
- Perform multiple capture runs against different versions of the live backend, so network fixtures are created for all these versions
- Define multiple jobs in the GitHub workflow that execute a stub run against these different versions
Configuration
A workflow as described above consists of a few moving parts, which are described in more details below. However, running the d2-utils-cypress install
command should create all required files.
Cypress
Environment variables
The Network Shim depends on a few cypress environment variables. These can be set using the --env
flag when interacting with the cypress CLI, but base values should be provided as well in via the following configuration files, see the cypress docs for more information.
./cypress.config.js
This file is meant to be checked into source control, so should not contain any sensitive or machine-specific information.
module.exports = defineConfig({
projectId: 'PROJECT_ID',
video: false,
e2e: {
setupNodeEvents: (on, config) => config,
baseUrl: 'http://localhost:3000',
},
env: {
dhis2DataTestPrefix: 'DATA_TEST_PREFIX',
networkMode: 'live', // default mode for the Network Shim
dhis2ApiVersion: 'value_from_prompt',
},
})
Notes:
- Only the
env
fields are required directly by the Network Shim, but other fields are included in this example since they are required to have a working cypress setup.
./cypress.env.json
This file is meant to be git-ignored and we can use this to set local environment variable that may contain sensitive or machine-dependent information.
{
"dhis2Username": "USERNAME", // secret
"dhis2Password": "PASSWORD", // secret
"dhis2BaseUrl": "DHIS2_BASE_URL" // machine specific
}
Note that dhis2Username
and dhis2Password
are not required directly by the Network Shim, but by the enableAutologin
setup. They are included in this example since they will be needed for most use-cases.
Plugin setup
The networkShim
plugin should be imported and initialised in the
setupNodeEvents function in the cypress config file (for more information about
the config file, see Getting started):
import { networkShim } from '@dhis2/cypress-plugins'
// ...
async function setupNodeEvents(on, config) {
// ...
networkShim(on, config)
// ...
}
// ...
The networkShim
plugin accepts a second options
argument. For most DHIS2
platform apps the default options of the networkShim
will be correct so there
will be no need to pass a second argument.
For most DHIS2 Cypress test suites the enableAutoLogin
plugin can help reduce
repetitive login steps in tests. In turn, the enableAutoLogin
setup utility
requires the chromeAllowXSiteCookies
plugin when using Chrome. So a typical
DHIS2 app will use those plugin alongside the networkShim
.
Below is an example of a cypress config file with options
supplied to the
networkShim
and used in conjunction with chromeAllowXSiteCookies
:
const {
networkShim,
chromeAllowXSiteCookies,
} = require('@dhis2/cypress-plugins')
const { defineConfig } = require('cypress')
const networkShimOptions = {
hosts: ['https://main-api.com', 'https://secondary-api.com'],
staticResources: [
'/endpoint-which-always-returns-the-same-data',
'/another-endpoint-which-always-returns-the-same-data',
],
}
async function setupNodeEvents(on, config) {
chromeAllowXSiteCookies(on, config)
networkShim(on, {
...config,
...networkShimOptions,
})
return config
}
module.exports = defineConfig({
e2e: {
setupNodeEvents,
baseUrl: 'http://localhost:3000',
},
})
Support file
The enableNetworkShim
setup should be imported and executed in the cypress support file. This function accepts no arguments. Typically, in a DHIS2 platform app, the enableAutoLogin
setup is required so individual tests don't require login steps, which means the file should look like this:
import { enableAutoLogin, enableNetworkShim } from '@dhis2/cypress-commands'
enableAutoLogin()
enableNetworkShim()
Package scripts and dependencies
./package.json
The following devDependencies
are required:
"devDependencies": {
"@dhis2/cypress-commands": "^#.#.#",
"@dhis2/cypress-plugins": "^#.#.#",
},
And it is recommended to add some convenience commands to the scripts
section, for example:
"scripts": {
"cy:start": "REACT_APP_NODE_ENV=test BROWSER=none yarn start",
"cy:prepare": "start-server-and-test 'yarn cy:start' http://localhost:3000",
"cy:open: "yarn cy:prepare 'yarn cypress open --env networkMode=live'",
"_cy:live": "yarn cypress run --env networkMode=live",
"cy:live: "yarn cy:prepare 'yarn _cy:live'",
"_cy:capture": "yarn cypress run --env networkMode=capture",
"cy:capture": "yarn cy:prepare 'yarn _cy:capture'",
"_cy:stub": "yarn cypress run --env networkMode=stub",
"cy:stub": "yarn cy:prepare 'yarn _cy:stub'",
},
The reason to keep cy:live
and _cy:live
separate is to be able to run
_cy:live
again and again during development without having to wait for the app
to start every time you want to run the tests on the cli again.
Note that cy:stub
here is meant to perform a local stub-run, the stub run on CI should be configured differently as explained in the GitHub workflow section.
Some notes:
- These example scripts assume
start-server-and-test
is available. If this is not the case, run:yarn add --dev start-server-and-test
- Setting
BROWSER=none
before runningyarn start
simply means that no browser tab is going to be opened/activated once the app is ready. - In the example above no
dhis2ApiVersion
is passed as a cypress env variable, even though the Network Shim relies on this. This works because that variable is declared in./cypress.env.json
so that value is used. See the cypress docs for more info. - The example above only takes into account testing against a single backend version. If an additional v36 version of DHIS2 core was available on
http://localhost:8081
and the test-suite should be executed against that backend as well, the following scripts could be added:"_cy:capture-36": "yarn cypress run --env networkMode=capture,dhis2BaseUrl=http://localhost:8081,dhis2ApiVersion=36",
"cy:capture-36: "yarn cy:prepare 'yarn _cy:capture-36'",
"_cy:stub-36": "yarn cypress run --env networkMode=stub,dhis2BaseUrl=http://localhost:8081,dhis2ApiVersion=36",
"cy:stub-36: "yarn cy:prepare 'yarn _cy:stub-36'",
GitHub workflow
At DHIS2 we use GitHub Actions for CI/CD, and we provide a workflow template for DHIS2 platform apps here. A typical DHIS2 platform-app will probably use the full dhis2-verify-app
workflow and store this in ./.github/workflows/dhis2-verify-app.yml
. Once the Network Shim has been configured and a capture run has been executed, all that is needed is to uncomment the e2e job.
Below is an illustration of such a job to provide some additional info:
- The job uses the
cypress-io/github-action@v2
GitHub action. - The required environment variables are set via the
env
parameter, note that:- They need to be prefixed with
CYPRESS_
. - The
dhis2Username
anddhis2Password
variables are not needed because we only do a stub run on CI.
- They need to be prefixed with
- To perform cross-version testing, more steps like this could be added, specifying a different value for
CYPRESS_dhis2BaseUrl
andCYPRESS_dhis2ApiVersion
. - In the example below
record
andparallel
have been enabled. This will only work if the project has been registered at the cypress dashboard and has a valid record key.
e2e:
runs-on: ubuntu-latest
if: "!github.event.push.repository.fork && github.actor != 'dependabot[bot]'"
strategy:
fail-fast: false
matrix:
containers: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 16.x
- name: End-to-End tests
uses: cypress-io/github-action@v2
with:
start: yarn cy:start
wait-on: 'http://localhost:3000'
wait-on-timeout: 300
record: true
parallel: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
CYPRESS_dhis2BaseUrl: http://localhost:8080
CYPRESS_dhis2ApiVersion: 37
CYPRESS_networkMode: stub
Github actions were used as an example in these docs, but it is also possible to setup cypress test-runs with these plugins on other CI solutions.