Cypress Testing Integrated with Gradle and Spring Boot

Cypress is a great testing framework for “anything that runs in a browser” allowing for clean, maintainable end to end tests. However, these tests can difficult and annoying to for developers to run, especially those who aren’t front end specialists. The following covers getting existing Cypress tests integrated and easily running within the Gradle-based build system of a Spring Boot project.

The advantages of this approach include:

  • ./gradlew build will run these tests – no additional commands are necessary
  • Developers will run these tests more often due to the ease of running them
  • No additional scripting (such as GitLab CI jobs or GitHub actions) is necessary. Since continuous integration already runs the build (including tests), it will naturally also run this new test.
  • Because this approach uses Docker orchestrated by testcontainers, developers don’t even need to install Cypress.

CypressTest as written, assumes:

  • The Gradle build has a dependency declared on testcontainers:
dependencies {
	implementation(platform("org.testcontainers:testcontainers-bom:1.16.0"))
	testImplementation "org.testcontainers:testcontainers"
}
  • The frontend application source is located in the frontend directory. That directory contains the usual content, including package.json and the cypress directory.
  • JUnit 5 is the test framework
  • Gradle is the build system and gradlew is available
  • The application uses Spring Boot 2.x (tested up to 2.5.4)
  • The Gradle build has gradle-node-plugin setup
  • Gradle has a yarnCypressVersion task defined that looks like this:
task yarnCypressVersion(type: YarnTask) {
	args = ['cypress', '--version']
}
  • In src/test/resources/application.yml, an internal.datasource.url property is defined. For example, if using a postgres database run by testcontainers:
# ?TC_TMPFS=/var/lib/postgresql/data:rw is a performance optimization to use a tmpfs for postgres data accelerating startup/runtime, see https://www.testcontainers.org/modules/databases/jdbc/
internal.datasource.url: jdbc:tc:postgresql:11:///?TC_TMPFS=/var/lib/postgresql/data:rw
spring.datasource.url: ${internal.datasource.url}

However, these assumptions can be changed with fairly minor adjustments to the test.

CypressTest ensures that the same version of Cypress is used to run the tests as is defined in the Yarn dependency so CypressTest is consistent with manual execution of these tests.

import static org.assertj.core.api.Assertions.assertThat;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.testcontainers.Testcontainers;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.SelinuxContext;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;

import lombok.extern.slf4j.Slf4j;

// if the Cypress tests end up writing to the database, a separate database instance must be used to ensure this test doesn't pollute the expected database state of other tests that run after it. &TC_REUSABLE=true ensures that.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {"spring.datasource.url=${internal.datasource.url}&TC_REUSABLE=true"})
@Slf4j
/* default */ class CypressTest {
	@LocalServerPort
	private int port;

	private static String cypressVersion;

	@BeforeAll
	private static void beforeAll() throws Exception {
		cypressVersion = getCypressVersion();
		log.info("Tests will run using Cypress version {}", cypressVersion);
	}

	@Test
	/* default */ void testCypress() throws Exception {
		Testcontainers.exposeHostPorts(port); // allow the container to access the running web application
		try (GenericContainer<?> container = new GenericContainer<>("cypress/included:" + cypressVersion)) {
			container.addFileSystemBind(Path.of(".").toAbsolutePath().toString(), "/src", BindMode.READ_WRITE, SelinuxContext.SHARED);
			container
				.withLogConsumer(new Slf4jLogConsumer(log))
				.withEnv("CYPRESS_baseUrl", String.format("https://%s:%d", GenericContainer.INTERNAL_HOST_HOSTNAME, port))
				.withWorkingDirectory("/src/frontend")
				.withStartupCheckStrategy(
						new OneShotStartupCheckStrategy().withTimeout(Duration.ofHours(1))
						).start();
			assertThat(container.getLogs()).contains("All specs passed!");
		}
	}

	/** Get the installed version of Cypress.
	 *
	 * This approach ensures that as the version of Cypress specified in package management changes,
	 * this tests will always use the same version.
	 * @return installed version of Cypress.
	 * @throws Exception if something goes wrong
	 */
	private static String getCypressVersion() throws Exception {
		String cmd = "./gradlew";
		if (System.getProperty("os.name").startsWith("Win")) {
			cmd = "./gradlew.bat";
		}
		final Process process = Runtime.getRuntime().exec(new String[]{cmd, "yarnCypressVersion"}, null, new File("."));
		Assert.state(process.waitFor() == 0,"Cypress version command did not complete successfully");
		final String output = StreamUtils.copyToString(process.getInputStream(), StandardCharsets.UTF_8);
		final Matcher matcher = Pattern.compile("Cypress package version: (?<version>\\d+(?:\\.\\d+)+)").matcher(output);
		Assert.state(matcher.find(), "Could not determine Cypress version from command output. Output: " + output);
		return matcher.group("version");
	}
}

Next Steps

The same Docker/testcontainers approach can be used to integrate other tools and tests into the build process. For instance, another great addition is Lighthouse Performance Testing.

CC BY-SA 4.0 Cypress Testing Integrated with Gradle and Spring Boot by Craig Andrews is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.