Running Cypress Tests on Pull Requests
- Kubernetes cluster set up that is connected to Signadot. See quickstart for details.
- HotRod application installed in the cluster. See quickstart for details.
Overview
End-to-end (E2E) tests are often run in staging environments after code merges. However, this can delay finding issues until late in the process. Signadot enables you to shift these tests to the Pull Request (PR) stage by leveraging Kubernetes and isolated Sandboxes. This approach allows for quicker feedback and helps catch bugs early.
In this tutorial, we’ll use Cypress with Signadot to run E2E tests on PRs, isolating each change for precise validation before merging.
App & Test Setup
Here, we’ll use the HotROD application—a simple
ride-sharing app. It includes four main services: frontend
, location
, driver
, and
route
. Each service handles a specific part of the app’s functionality, allowing users to
request rides, check locations, and receive estimated times of arrival.
Our focus will be on the frontend
service. We’ll set up Cypress tests that interact with
this service, ensuring that core user actions, like requesting a ride, work correctly. This
setup helps verify the app’s critical paths so you can confidently merge new code without
breaking essential functions.
The codes used in this tutorial are available on the HotRod repository. You can clone the repository to follow along.
Cypress Test
We'll proceed with the following Cypress test. The test simulates a user's interaction with the HotRod application by requesting a ride and verifying that the application responds correctly.
describe('HotRod E2E Spec', () => {
it('should request a ride successfully', () => {
// Request a ride
cy.requestRide('1', '123');
// Verify that a driver has been assigned
cy.contains(/Driver (.*) arriving in (.*)./).should('be.visible');
// Check that the route is being resolved
cy.contains('Resolving routes').should('be.visible');
});
});
In this test:
- We use the custom command
cy.requestRide('1', '123')
to simulate a user requesting a ride from location 1 to location 123. - After requesting a ride, we verify that the application displays a message indicating that a driver is arriving.
- We also check that the application shows "Resolving routes" to confirm that the route calculation is in progress.
This test focuses on verifying the core functionality of requesting a ride and ensuring that the user receives the expected feedback from the application.
Adding a Custom Command
To streamline our testing process, we created a custom command requestRide()
, for
requesting rides:
// Request a HotRod ride
Cypress.Commands.add('requestRide', (from, to) => {
var frontendURL = '<http://frontend>.' + Cypress.env('HOTROD_NAMESPACE') + ':8080';
cy.visit(frontendURL);
cy.get(':nth-child(1) > .chakra-select__wrapper > .chakra-select').select(from);
cy.get(':nth-child(3) > .chakra-select__wrapper > .chakra-select').select(to);
cy.get('.chakra-button').click();
})
This command makes requesting a ride simpler by encapsulating the necessary steps into
one reusable function. It intercepts requests to inject the SIGNADOT_ROUTING_KEY
header,
directing traffic to the appropriate Sandbox for testing.
The command navigates to the frontend service URL, selects the dropdown options, and submits the ride request.
We’ll import this custom command in the cypress/support/e2e.js
file,
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// <https://on.cypress.io/configuration>
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
This setup consolidates commands for easier maintenance, allowing you to modify them in one place if needed.
Configuring Cypress
Now, let’s configure Cypress with environment variables in cypress.config.js to enable flexible testing across different environments:
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
video: true,
experimentalStudio: true,
env: {
HOTROD_NAMESPACE: '',
SIGNADOT_ROUTING_KEY: '',
},
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: "http://frontend.hotrod.svc:8080"
},
});
In this configuration:
- baseUrl: Setting the
baseUrl
to http://frontend.hotrod.svc:8080 tells Cypress to use this URL as the base for allcy.visit()
commands. This URL is the in-cluster DNS address for the HotRod frontend service within Kubernetes. - Environment Variables: The env object includes variables like
HOTROD_NAMESPACE
andSIGNADOT_ROUTING_KEY
that help Cypress tests adapt to different environments and Sandboxes without changing the test code.
Injecting Routing Context
To effectively test changes within a Sandbox, we’ll need to ensure that Cypress directs requests
to the correct environment. Signadot uses a
routing key to manage this, and we’ll
inject the SIGNADOT_ROUTING_KEY
into our tests to help isolate each test run to its designated
Sandbox.
In the custom command cy.requestRide, we have an intercept to inject the routing key:
// Request a HotRod ride
Cypress.Commands.add('requestRide', (from, to) => {
var frontendURL = '<http://frontend>.' + Cypress.env('HOTROD_NAMESPACE') + ':8080';
// inject routing key
cy.intercept(frontendURL + '/*', (req) => {
req.headers['baggage'] += ',sd-routing-key=' + Cypress.env('SIGNADOT_ROUTING_KEY');
})
cy.visit(frontendURL);
cy.get(':nth-child(1) > .chakra-select__wrapper > .chakra-select').select(from);
cy.get(':nth-child(3) > .chakra-select__wrapper > .chakra-select').select(to);
cy.get('.chakra-button').click();
})
Here, we inject the SIGNADOT_ROUTING_KEY
into the request header. This key tells Signadot which
Sandbox to route the request to, ensuring it aligns with the changes associated with the PR.
We can control the routing dynamically by setting SIGNADOT_ROUTING_KEY
as an environment variable
in cypress.config.js
. This means we don't need to hardcode the routing context into individual
tests. It makes it easier to run tests in different Sandboxes or environments without changing your
test code.
Testing Using Signadot
With the Cypress tests set up, we can run them in our Kubernetes cluster using Signadot. To do this, let’s create a Job Runner Group (JRG), which manages the test jobs within the cluster, allowing us to execute tests on demand.
Setting Up the Job Runner Group (JRG)
Let’s create a Job Runner Group configuration file. Save it as .signadot/testing/cypress-runner.yaml
:
name: cypress
spec:
cluster: "@{cluster}"
labels:
env: "@{env}"
namespace: signadot-tests
image: cypress/included:latest
jobTimeout: 30m
scaling:
manual:
desiredPods: 1
Apply the configuration to the cluster by running:
signadot jrg apply \
-f .signadot/testing/cypress-runner.yaml \
--set cluster=<your-cluster-name>
Replace <your-cluster-name>
with the name of your Kubernetes cluster. This command deploys the
Job Runner Group, preparing it to run Cypress tests on PRs.
Defining the Test Jobs
Next, let’s define a job to run the hotrod-e2e.cy.js
Cypress test. Create a new job
configuration file named .signadot/testing/e2e-tests-job.yaml
:
spec:
namePrefix: hotrod-cypress-e2e
runnerGroup: cypress
script: |
#!/bin/bash
set -e
# Clone the git repo
echo "Cloning signadot repo"
git clone --single-branch -b "@{branch}" \
https://github.com/signadot/hotrod.git
# Run cypress test
cd hotrod
export CYPRESS_SIGNADOT_ROUTING_KEY=$SIGNADOT_ROUTING_KEY
npx cypress run
routingContext:
sandbox: "@{sandbox}"
uploadArtifact:
- path: hotrod/cypress/videos/hotrod-demo.cy.js.mp4
- path: hotrod/cypress/videos/hotrod-e2e.cy.js.mp4
In this configuration:
namePrefix
provides a prefix for the job's name.runnerGroup
assigns the job to the Job Runner Group cypress.script
contains the steps to clone the HotRod repository, set environment variables, and run the Cypress tests.uploadArtifact
specifies files to save after the test.
The SIGNADOT_ROUTING_KEY
variable is automatically provided by Signadot when the job is executed.
When you submit a job using the signadot job submit
command, Signadot creates a unique routing key
associated with the sandbox (or baseline) specified in the job's routingContext
. This routing key
is then made available to the job's execution environment as an environment variable named
SIGNADOT_ROUTING_KEY
.
In the job script above, we export CYPRESS_SIGNADOT_ROUTING_KEY
and assign it the value of
$SIGNADOT_ROUTING_KEY
. This makes the routing key available to Cypress via
Cypress.env('SIGNADOT_ROUTING_KEY')
, which is used in the custom command to inject the routing key
into HTTP requests.
Running Tests on the Baseline
Let's run the Cypress tests on the baseline (without Sandbox). This will validate our app's stable version before introducing PR changes. Submit the job using the following command:
signadot job submit \
-f .signadot/testing/e2e-tests-job.yaml \
--set sandbox="" \
--set branch="main" \
--attach
By setting sandbox=""
, we instruct the job to run on the baseline (no Sandbox). This helps ensure
the app is fully functional in its current state.
Running Tests on a Sandbox
To validate changes from a Pull Request, let’s run tests within an isolated Sandbox. For example,
if we modify the driver
service, let’s configure a Sandbox to use a custom image for testing.
Create the Sandbox configuration in .signadot/testing/sandbox.yaml
:
name: new-driver
spec:
description: Test the updated driver service
cluster: "@{cluster}"
forks:
- forkOf:
kind: Deployment
namespace: hotrod-istio
name: driver
customizations:
images:
- image: >-
signadot/hotrod:8b99a5b2ef04c4219e42f3409cd72066279fd0e4-linux-amd64
container: hotrod
- Run with UI
- Run with CLI
Click the button below to open and run this spec on Create Sandbox UI.
Run the below command using Signadot CLI.
# Save the sandbox spec as `new-driver.yaml`.
# Note that <cluster> must be replaced with the name of the linked cluster in
# signadot, under https://app.signadot.com/settings/clusters.
% signadot sandbox apply -f ./new-driver.yaml --set cluster=<cluster>
Created sandbox "new-driver" (routing key: dxux1yyzbrb0g) in cluster "<cluster name>".
Waiting (up to --wait-timeout=3m0s) for sandbox to be ready...
✓ Sandbox status: Ready: All desired workloads are available.
Dashboard page: https://app.signadot.com/sandbox/id/dxux1yyzbrb0g
The sandbox "new-driver" was applied and is ready.
Once the Sandbox is created, submit the test job with:
signadot job submit \
-f .signadot/testing/e2e-tests-job.yaml \
--set sandbox=new-driver \
--set branch="main" \
--attach
This command runs the Cypress tests in the isolated environment provided by the Sandbox. If the
driver
service changes cause issues, we’ll identify it before reaching the main application.
This enables us to get rapid feedback and ensure quick fixes.
Conclusion
In this tutorial, we learned how to use Signadot Sandboxes to run Cypress tests on Pull Requests. This enables us to catch issues early by testing isolated changes within Kubernetes. The approach helps streamline development and ensures smoother, more reliable releases.
Integrating this workflow into your CI/CD pipeline will automate testing at the PR stage. It will improve code quality and speed up development. For more information on configuring Signadot and Cypress, refer to the Signadot Documentation and the Cypress Documentation.