The goal of this project is to make it easy to start Cypress tests via Testcontainers.
-
Create
src/test/e2e
directory in your project. -
Run
npm init
in that directory. This will generate apackage.json
file. -
Run
npm install cypress --save-dev
to install Cypress as a development dependency. -
Run
npx cypress open
to start the Cypress application. It will notice that this is the first startup and add some example tests. -
Run
npm install cypress-multi-reporters mocha mochawesome --save-dev
to install Mochawesome as a test reporter. Testcontainers-cypress will use that to parse the results of the Cypress tests. -
Update
cypress.config.js
as follows:const { defineConfig } = require('cypress') module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost:8080/', reporter: 'cypress-multi-reporters', reporterOptions: { configFile: 'reporter-config.json' } } })
-
Create a
reporter-config.json
file (next tocypress.json
) and ensure it contains:{ "reporterEnabled": "spec, mochawesome", "mochawesomeReporterOptions": { "reportDir": "cypress/reports/mochawesome", "overwrite": false, "html": false, "json": true } }
Tip
|
Add the following to your
|
Use this dependency if you use Maven:
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>testcontainers-cypress</artifactId>
<version>${tc-cypress.version}</version>
<scope>test</scope>
</dependency>
Add src/test/e2e
as a test resource directory:
<project>
<build>
...
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
<testResource>
<directory>src/test/e2e</directory>
<targetPath>e2e</targetPath>
</testResource>
</testResources>
</build>
</project>
For Gradle, use the following dependency:
testImplementation 'io.github.wimdeblauwe:testcontainers-cypress:${tc-cypress.version}'
Process src/test/e2e
as a test resource directory and copy it into buid/resources/test/e2e
.
processTestResources {
from("src/test/e2e") {
exclude 'node_modules'
into("e2e")
}
}
The default GatherTestResultsStrategy
assumes a Maven project and therefore you need to create a custom strategy that fits the Gradle layout.
MochawesomeGatherTestResultsStrategy gradleTestResultStrategy = new MochawesomeGatherTestResultsStrategy(
FileSystems.getDefault().getPath("build", "resources", "test", "e2e", "cypress", "reports", "mochawesome"));
new CypressContainer()
.withGatherTestResultsStrategy(gradleTestResultStrategy)
The library is not tied to Spring Boot, but I will use the example of a @SpringBootTest
to explain how to use it.
Suppose you have a Spring Boot application that has server-side rendered templates using Thymeleaf, and you want to write some UI tests using Cypress. We want to drive all this from a JUnit based test, so we do the following:
-
Have Spring Boot start the complete application in a test. This is easy using the
@SpringBootTest
annotation on a JUnit test. -
Expose the web port that was opened towards Testcontainers so that Cypress that is running in a Docker container can access our started web application.
-
Start the Docker container to run the Cypress tests.
-
Wait for the tests to be done and report the results to JUnit.
Start by writing the following JUnit test:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //(1)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //(2)
public class CypressEndToEndTests {
@LocalServerPort //(3)
private int port;
@Test
void runCypressTests() throws InterruptedException, IOException, TimeoutException {
Testcontainers.exposeHostPorts(port); //(4)
try (CypressContainer container = new CypressContainer().withLocalServerPort(port)) { //(5)
container.start();
CypressTestResults testResults = container.getTestResults(); //(6)
if (testResults.getNumberOfFailingTests() > 0) {
fail("There was a failure running the Cypress tests!\n\n" + testResults); //(7)
}
}
}
}
-
Have Spring Boot start the full application on a random port.
-
Tell Spring Boot to not configure a test database, Because we use a real database (via Testcontainers obviously :-) ).
-
Have Spring inject the random port that was used when starting the application.
-
Ensures that the container will be able to access the Spring Boot application that is started via @SpringBootTest
-
Create the
CypressContainer
and pass in theport
so the base URL that Cypress will use is correct. -
Wait on the tests and get the results.
-
Check if there have been failures in Cypress. If so, fail the test.
If you are using JUnit 5, then you can use a @TestFactory
annotated method so that it looks like there is a JUnit test
for each of the Cypress tests.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class CypressEndToEndTests {
@LocalServerPort
private int port;
@TestFactory // (1)
List<DynamicContainer> runCypressTests() throws InterruptedException, IOException, TimeoutException {
Testcontainers.exposeHostPorts(port);
try (CypressContainer container = new CypressContainer().withLocalServerPort(port)) {
container.start();
CypressTestResults testResults = container.getTestResults();
return convertToJUnitDynamicTests(testResults); // (2)
}
}
@NotNull
private List<DynamicContainer> convertToJUnitDynamicTests(CypressTestResults testResults) {
List<DynamicContainer> dynamicContainers = new ArrayList<>();
List<CypressTestSuite> suites = testResults.getSuites();
for (CypressTestSuite suite : suites) {
createContainerFromSuite(dynamicContainers, suite);
}
return dynamicContainers;
}
private void createContainerFromSuite(List<DynamicContainer> dynamicContainers, CypressTestSuite suite) {
List<DynamicTest> dynamicTests = new ArrayList<>();
for (CypressTest test : suite.getTests()) {
dynamicTests.add(DynamicTest.dynamicTest(test.getDescription(), () -> {
if (!test.isSuccess()) {
LOGGER.error(test.getErrorMessage());
LOGGER.error(test.getStackTrace());
}
assertTrue(test.isSuccess());
}));
}
dynamicContainers.add(DynamicContainer.dynamicContainer(suite.getTitle(), dynamicTests));
}
}
-
Use the
@TestFactory
annotated as opposed to the@Test
method -
Use the
CypressTestResults
to generateDynamicTest
andDynamicContainer
instances
If the Cypress tests look like this:
context('Todo tests', () => {
it('should show a message if there are no todo items', () => {
cy.request('POST', '/api/integration-test/clear-all-todos');
cy.visit('/todos');
cy.get('h1').contains('TODO list');
cy.get('#empty-todos-message').contains('There are no todo items');
});
it('should show all todo items', () => {
cy.request('POST', '/api/integration-test/prepare-todo-list-items');
cy.visit('/todos');
cy.get('h1').contains('TODO list');
cy.get('#todo-items-list')
.children()
.should('have.length', 2)
.should('contain', 'Add Cypress tests')
.and('contain', 'Write blog post');
})
});
Then running the JUnit test will show this in the IDE:
This makes it a lot easier to see which Cypress test has failed.
The CypressContainer
instance can be customized with the following options:
Method | Description | Default |
---|---|---|
|
Allows to specify the docker image to use |
|
|
Set the port where the server is running on. It will use http://host.testcontainers.internal as hostname with the given port as the Cypress base URL. For a |
|
|
Set the full server url that will be used as base URL for Cypress. |
|
|
Set the browser to use when running the tests (E.g. |
|
|
Sets the test(s) to run. This can be a single test (e.g. |
|
|
Passes the |
|
|
Passes the |
|
|
Set the relative path of where the cypress tests are (the path is the location of where the |
|
|
Set the maximum timeout for running the Cypress tests. |
|
|
Set the |
|
|
Set the path (relative to the root of the project) where the Mochawesome reports are put. |
|
|
Set if the Cypress test reports should be automatically cleaned before each run or not. |
|
Testcontainers-cypress | Testcontainers | Cypress |
---|---|---|
When running on a mac with an M1 or M2 chip, only the electron browser is included in the underlying cypress image this project uses. For more info please read this.
There is a workaround, however, which involves following steps:
-
install rosetta 2:
softwareupdate --install-rosetta
-
Enable rosetta emulation in docker desktop for mac:
-
General > Use virtualization framework
-
Features in Development > Use rosetta for x86/amd64 simulation on apple silicon
-
-
Pull the platform specific (linux/amd64) image for the cypress-included which you are using. For example: docker pull --platform linux/amd64 cypress/included:13.3.0
Links to blog or articles that cover testcontainers-cypress:
- Example usage of testcontainers cypress
-
Good introduction on how to get started.
- Testcontainers-cypress release 0.4.0
-
Shows how to run tests on multiple browsers with JUnit
-
Builds are done with GitHub Actions: https://github.com/wimdeblauwe/testcontainers-cypress/actions
-
Code quality is available via SonarQube: https://sonarcloud.io/dashboard?id=io.github.wimdeblauwe%3Atestcontainers-cypress
-
SNAPSHOT versions are put on https://oss.sonatype.org/content/repositories/snapshots
-
All releases can be downloaded from https://oss.sonatype.org/content/groups/public
To release a new version of the project, follow these steps:
-
Update
pom.xml
with the new version (Usemvn versions:set -DgenerateBackupPoms=false -DnewVersion=<VERSION>
) -
Commit the changes locally.
-
Tag the commit with the version (e.g.
1.0.0
) and push the tag. -
Create a new release in GitHub via https://github.com/wimdeblauwe/testcontainers-cypress/releases/new
-
Select the newly pushed tag
-
Update the release notes. This should automatically start the [release action](https://github.com/wimdeblauwe/testcontainers-cypress/actions).
-
-
Update
pom.xml
again with the nextSNAPSHOT
version. -
Close the milestone in the GitHub issue tracker.