Run Cypress E2E tests in isolated Sandboxes per PR with Signadot to catch bugs early—before merging.
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.
hotrod/cypress/e2e/hotrod-e2e.cy.js
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:
hotrod/cypress/support/commands.js
// 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,
hotrod/cypress/support/e2e.js
// ***********************************************************
// 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:
hotrod/cypress.config.js
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_KE
Y 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:
hotrod/cypress/support/commands.js
// 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
:
hotrod/.signadot/testing/cypress-runner.yaml
name: cypress
spec:
cluster: my-cluster
labels:
team: frontend
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 routingContex
t. 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
Click here to open and run this spec on Create Sandbox UI.
Run with CLI:
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.
Top comments (0)