E2E Tests
This repo is configured for end-to-end testing with Cypress and the CI will run using a blank state of Rancher executed locally. The aim is however to enable also tests using remote instances of Ranchers.
Because of this, we extend the Cypress best practices, so be sure to read them before write any test.
Quick start
This covers running E2E tests locally in a vanilla environment
Pre-requisites
- Instance of Rancher deployed and set up (passed setup pages)
- Dashboard running locally at the default address (
https://localhost:8005
) pointing to the set up rancher
Run the tests
This will start the cypress test runner, where you can select which tests to run
TEST_PASSWORD=<rancher admin password> TEST_SKIP=setup yarn cy:open
Initial Setup
For the cypress test runner to consume the UI, you should specify the environment variables. This may change based on the type of tests you may want to run.
Setup for remote tests
For tests against a deployed Rancher, e.g. on Digital Ocean, mainly for analyzing the project current state.
TEST_USERNAME
, defaultadmin
TEST_PASSWORD
, user password or custom during first Rancher runTEST_BASE_URL
, the address of your instanceTEST_SKIP=setup
, we avoid setup as your instance is already set
Note: If you want to generate code coverage information, you must enable code instrumentation by setting
TEST_INSTRUMENT
totrue
.
Finally run one of the 2 commands:
yarn cy:open
, if you want to select the testsyarn cy:run
, if you want to run ALL the tests
For further information, consult official documentation.
Setup for local tests
These types of tests are aimed for development and updates.
NOTE: Local setup of Rancher do not work on Mac with M1 chips.
API
, the address of your server (e.g. DO), it may be local or hosted (e.g. you have Mac M1)TEST_USERNAME
, defaultadmin
TEST_PASSWORD
, user password or custom during first Rancher runCATTLE_BOOTSTRAP_PASSWORD
, initialization password which will also be used asadmin
user password (do not pickadmin
as password as it generates issues)TEST_BASE_URL=https://localhost:8005
TEST_SKIP=setup
, avoid to execute bootstrap setup tests for already initialized Rancher instances, it has to be toggled in case of new instances
You will have to run your local instance at this point:
yarn dev
Finally run one of the 2 commands:
yarn cy:open
, if you want to select the tests from the browseryarn cy:run
, if you want to run ALL the tests in background
For further information, consult official documentation.
Setup for dashboard purposes ONLY
If you want your tests to be tracked on Cypress dashboards you will have to enable the following:
TEST_PROJECT_ID
// Project ID used by Cypress/Sorry cypress to run the testsTEST_RUN_ID
(optional) // Identifier for your dashboard run, default value is timestamp
Skip and only features
Existing TEST_SKIP_SETUP
logic has been replaced with something more generic included in the cypress.ts
script/utility.
It is now possible to skip features by using the TEST_SKIP
env var, e.g. TEST_SKIP=setup
.
Alternatively is possible to solely run a specific feature by using the TEST_ONLY
env var, e.g. TEST_ONLY=setup
.
The features are folder name based and can be found in cypress/e2e/tests/pages
.
E2E Dashboard
Self-hosted: Sorry Cypress
Link to the dashboard: http://139.59.134.103:8080/
E2E tests can be added and displayed in a dashboard by defining the project ID with the env var TEST_PROJECT_ID
, then run the script:
yarn cy:run:sorry
Cypress dashboard installation guide
The setup is done using a cloud hosting service and with its IP we configured the Sorry Cypress as indicated in the guide. The process is straightforward, except for the IP which is required to be overwritten within minio.yml
manifest as the default http://localhost
value generate CORS issues.
Cypress Dashboard
E2E tests can be displayed in Cypress dashboard by defining the project ID with the env var TEST_PROJECT_ID
, then run the script by passing the parameters
yarn cy:run --record --key YOUR_RECORD_KEY_HERE
These values are provided when you create a new project within Cypress dashboard or within Project settings
.
It's also possible to run a workflow in GitHub Actions E2E test using these values to record on personal dashboards.
Local and CI/prod run
It is possible to start the project and run all the tests at once with a single command. There's however a difference between dev
and production
run. The first will not require an official certificate and will build the project in dist
, while the production will enable all the SSL configurations to run encrypted.
yarn e2e:pre-dev
, to optionally initialize Docker and build the project, if not already doneyarn e2e:dev
, single run local developmentyarn e2e:pre-prod
, to optionally initialize Docker and build the project, required for GitHub Actionsyarn e2e:dev
, for production use case and CI, which will also restart Docker and build the project
Custom Commands
As Cypress common practice, some custom commands have been created within command.ts
file to simplify the development process. Please consult Cypress documentation for more details about when and how to use them.
Worth mentioning the cy.getId()
and cy.findId()
commands, as it is mainly used to select elements. This would require to add data-testid
to your element inside the markup and optionally matchers.
Writing tests
Test specs should be grouped logically, normally by page or area of the Dashboard but also by a specific feature or component.
Tests should make use of common Page Object (PO) components. These can be pages or individual components which expose a useful set of tools, but most importantly contain the selectors for the DOM elements that need to be used. These will ensure changes to the underlying components don't require a rewrite of many many tests. They also allow parent components to easily search for children (for example easily finding all anchors in a section instead of the whole page). Given that tests are typescript it should be easy to explore the functionality.
Some examples of PO functionality
HomePage.gotTo()
new HomePagePo().checkIsCurrentPage()
new BurgerMenuPo().clusters()
new AsyncButtonPO('[data-testid="my-button"]').isDisabled()
new LoginPagePo().username().set('admin')
POs all inherit a root component.po
. Common component functionality can be added there. They also expose their core cypress (chainable) element.
Best Practices
data-testid
When selecting an element priority should be given to the attribute data-testid
, if this does not exist using a specific css selector can be used.
- In some cases, including lists, the data-testid is dynamically created with a context prefix or index, so check the DOM even if it code it's not obvious
Environment State - Pre / Post Test
Initial State
If the test needs a specific state it should not be assumed that the Rancher instance is in that state, it should be confirmed or setup before the test starts.
- Avoid using shaky foundations for a test. For example checking pods in the
cattle-system
namespace which will change a lot after Rancher is provisioned (and tests run immediately against it) - Test needs 500 events, tests create pods to create those events. 9/10 there's enough events, 1/10 there are not and the test will fail
- Test expects helm repos to be added and ready, however slow Rancher setup means these are in flux
Finishing State
If the tests have altered the state of the Rancher instance they should return it back to it's original state after the tests have run
Included (Examples)
- created resources
- language selected
- filtering by namespace
Not included
- Page the user is on
- Logged in state
Hooks
Utilize the beforeAll
and afterAll
hooks to setup the test, and then clean up afterwards. Be careful though, these won't run again if Cypress retries individual failed tests
Returning the environment to it's original state via afterAll is important to avoid subsequent tests being affected, for example by resources created during test execution.
Resources In tests
When the test needs to exercise the UI with resources, where possible, they should be actual resources. Where not possible they can be mocked locally (in a scalable way).
Resource Names
Names of resources should almost always come from the createE2EResourceName
command. This pre/post fixes a run id to the name and makes it really clear when e2e tests leave stale resources around. In the future it also opens us up to automatically cleaning them up
Creating resources
Resources can either be created
- Preferred - Via cy commands calling the Rancher API
- Via the UI itself
Mocking Resources
cy.intercept
can be used to intercept http requests and return mock resources.
- Mock resources can be brittle and verbose, but are good for testing at scale
- Ideally we need a library of
create<ResourceType>
functions that can be called 100s of times if required (rather than hardcoding 100s of resources)
revision / resourceRevision
Mock resources should revision
(resource) and resourceRevision
(list) properties to CYPRESS_SAFE_RESOURCE_REVISION
. A value that's too low can results in CPU impactful spam
- UI makes a request to fetch a resource, mock resource with too low revision is provided
- UI tries to watch resource over websocket with the too low revision
- Rancher rejects the too low revision
- UI tries to fix this by fetching the resource, but the mock resource with too low revision is returned again
- Repeat ad nauseam
A revision will be too low depending on the Rancher instance.
Excluding a test
Track how to include Add a comment referencing an issue that will lead to the test being included again
Avoid skip
To exclude a test the jasmine .skip
notation can be used. However this can cause havoc with grep tags / sorry cypress ending in longer running test runs and fluff results.
It's better to just comment out the test instead.
Where's my Page Object?
Page Objects (POs) generally represent UI components or pages.
A UI component could be
- quite core
LabeledInput
- intermediate
ArrayList
(containsLabeledInput
s),shell/edit/networking.k8s.io.ingress/Certificate.vue
(containsArrayList
)
- singular at the page level
shell/edit/networking.k8s.io.ingress/index.vue
(containsnetworking.k8s.io.ingress/Certificate.vue
)
- generic, and singular at the page level
shell/components/ResourceDetail/index.vue
(containsshell/edit/networking.k8s.io.ingress/index.vue
)
A Page component could be more conceptual, for example the Edit Service
page (which under the hood is shell/pages/c/_cluster/_product/_resource/_id
).
When writing tests start by first searching at the page level and then working further down the stack towards the core component. Once a PO is found work back up the stack, implementing POs as required.
Example
Considering the chain above, if we were to test the Create/Edit Ingress's page's Certificate tabs Hosts list consider the DOM
<div data-v-9b95c6bc="" data-testid="array-list-box0" class="box">
<div data-v-9b95c6bc="" class="value">
<div data-v-33afc918="" data-v-9b95c6bc="" class="labeled-input edit compact-input">
<input data-v-33afc918="" type="text" placeholder="e.g. bar" autocomplete="off" autocapitalize="off" data-testid="labeled-input-0" class="no-label">
<div data-v-33afc918="" class="labeled-tooltip error hoverable"><i class="icon status-icon icon-warning has-tooltip" data-original-title="null"></i></div>
</input>
</div>
</div>
<div data-v-9b95c6bc="" class="remove">
<button data-v-9b95c6bc="" type="button" data-testid="remove-item-0" class="btn role-link">
Remove
</button>
</div>
</div>
- 1/10 - Not great
- in the spec file create a LabeledInput PO to access the first entry using the selector
data-testid="labeled-input-0"
- in the spec file create a LabeledInput PO to access the first entry using the selector
- 2/10 - Not much better
- in the spec file create a ArrayList PO to to access the first entry using the selector
data-testid="array-list-box0"
- in the spec file create a ArrayList PO to to access the first entry using the selector
- 3/10 - Still not there
- in
cypress/e2e/po/edit/ingress.po.ts
create functions that directly return either of above
- in
- 10/10 - Top of the class
- find the page level PO for the Ingress edit page
cypress/e2e/po/edit/ingress.po.ts
- this is where the access chain should start
- discover it has no way to access the Certificates component, so create a
cypress/e2e/po/edit/ingress/certificates.po.ts
calledIngressCertificatesPo
- Expose the new PO in
cypress/e2e/po/edit/ingress.po.ts
as a new method.certificates(): IngressCertificatesPo
- Expose the new PO in
- in
IngressCertificatesPo
create a new methodhostsArrayList
that returns anArrayList
- in
IngressCertificatesPo
create a methodhosts(index: number)
that returns a new LabelSelect given.hostsArrayList(index)
- this all means in the spec file we can do
<page po instance>.certificates.hosts(x)...
- find the page level PO for the Ingress edit page
The best case scenario means future test creates will be able to easily
- understand the ingress test
- write new or expands tests around external addresses, or other features on the page
Tips
The Cypress UI is very much your friend. There you can click pick tests to run, easily visually track the progress of the test, see the before/after state of each cypress command (specifically good for debugging failed steps), see https requests, etc.
Tests can also be restricted before cypress runs, or at run time, by prepending .only
to the run.
describe.only('Burger Side Nav Menu', () => {
beforeEach
it.only('Opens and closes on menu icon click', () => {
Data testid naming
While defining naming, always consider deterministic usage and rely on specific values. For cases where the content is required, e.g. select name specific elements as in cluster selection, consider use the contain()
method. Further guideline and explanation in the official documentation related section.
In case of complex component, define a prefix for your data-testid
with a the prop componentTestid
and a default value. This will help you to define unique value and composable identifier in case of more elements, as well to avoid custom term for each test if not necessary, e.g. no multiple elements.
E.g. given the action menu:
/**
* Inherited global identifier prefix for tests
* Define a term based on the parent component to avoid conflicts on multiple components
*/
componentTestid: {
type: String,
default: 'action-menu'
}
<li
v-for="(option, i) in options"
:key="opt.action"
:data-testid="componentTestid + '-' + i + '-item'"
>
Debugging
To summarize what defined in the documentation, the following modalities of debugging are provided:
debugger
flag.debug()
as chained commandcy.pause()
for analyzing the state of the test- Inspect commands in the Cypress dashboard to view the logs
.then(console.log)
to append the log to the resolved promise
These values are provided when you create a new project within Cypress dashboard or within Project settings
.
Coverage
Both unit and E2E tests generate coverage respectively with Jest and NYC. These values are generated on both PR and push to master
and release
after merging. The service used to display the values is Codecov and can be found here.
Special attention goes to the E2E as the code is instrumented with Babel and the configuration is set within Nuxt.js.
Note: To enable instrumentation required for code coverage, you must set the environment variable
TEST_INSTRUMENT
totrue
.
Cypress Tags (cypress-grep plugin)
[cypress-grep](https://github.com/cypress-io/cypress/tree/develop/npm/grep#cypressgrep)
is a plugin that is integrated with our project to group e2e tests by adding tags to them.
Reasons we are grouping our tests:
- To add test coverage for different types of users
- Split tests for different features
- Parallelization within CI
- In the future support sets of tests specific to the context they run in (PR CI, overnight, etc).
E2E with user role tags
Tags currently in use for the roles are @adminUser
and @standardUser
.
We use GREP_TAGS
and TEST_USERNAME
environment variables to execute all tests which contain @admin or @standardUser tags respectively:
E.g. when running locally:
GREP_TAGS=@adminUser TEST_USERNAME=admin yarn cy:run
OR
GREP_TAGS=@standardUser TEST_USERNAME=<standard user username> yarn cy:run
More info about cypress-grep plugin can be found here.
E2E with feature tags
As part of parallelization process and identification of features, tags have been added following the file tree structure of cypress/e2e/tests/pages
.
These are the added specs tags for each category: @components
, @navigation
, @charts
, @explorer
, @extensions
, @fleet
, @generic
, @globalSettings
, @manager
, @userMenu
, @usersAndAuths
.
E2E tests parallelization in CI
Tests in CI are executed in parallel within GitHub workflow, reducing drastically time and avoiding to slow down the machine as it may happen with the flag --parallel
of Cypress, which will use and need a machine with more cores.
This process is achieved using job matrix definitions with objects where we intersect user roles and features.
Current matrix state:
matrix:
role: [
{ username: 'admin', tag: '@adminUser' },
{ username: 'standard_user', tag: '@standardUser' }
]
features: [
['@navigation', '@extensions'],
['@charts'],
['@explorer'],
['@fleet'],
['@generic', '@globalSettings'],
['@manager'],
['@userMenu', '@usersAndAuths'],
['@components'],
]
In this case the tags defined for the process will then become GREP_TAGS=@adminUser+@generic
. This means that the tests will need to have both tags to be executed.
Some tests have been paired due GH limits of concurrent jobs and have then been defined as @adminUser+@generic @adminUser+@globalSettings
. It means that either one of the 2 combinations which be considered as filter criteria.
To summarize, space between tags is considered as AND
operator, while +
is considered as OR
operator.
To allow re-run of flaky tests only which may fail, the job is flagged as fail-fast: false
and will prevent to interrupt the others.
DISCLAIMER: It is not possible to execute multiple tests with the same ID and therefore for the temporary test initialization where we use the setup, the configuration will have extra tags such like @adminUserSetup
and @standardUserSetup
. This will be replaced with a script as planned.
Cypress Utilities
In case of logic used within the E2E, utilities can be defined in scripts/cypress.ts
and tested in the related file with unit tests.