UI integration tests for React applications with java
In the past, I wrote about how to tackle a major issue with UI end-to-end (E2E) tests, which is the problem of tests being unreliable or “flaky.” Another approach to address this problem is to shift towards using UI integration tests, which are lower in the testing pyramid. The benefits of using integration tests instead of E2E tests are:
- Faster execution time.
- Higher reliability.
However, there are also some downsides to using integration tests compared to E2E tests, including:
- Difficulty in maintaining mock data.
- Limited ability to detect certain defects.
I will use a simple React application as the test subject, which displays cat facts obtained from an external service.
import { useEffect, useState } from "react";
function App() {
const [fact, setFact] = useState("");
useEffect(() => {
fetch(process.env.REACT_APP_CAT_FACT_URL)
.then((response) => response.json())
.then((data) => setFact(data.fact))
.catch((error) => console.log(error));
}, []);
return (
<div className="App">
<h1>Cat Fact</h1>
<p data-test-id="fact-text">{fact}</p>
</div>
);
}
export default App;
React application has two modes to fetch from real service(https://catfact.ninja/fact) and to fetch from mock(http://localhost:8080/fact)
In the test, I want to verify that the user is able to view a fact about a cat. However, we face two challenges:
- Our application depends on an external service that we don’t own and control, and we don’t want to actually test it.
- Since the fact is random, our test can either be too abstract (not checking the actual fact) or flaky (varying results).
Both these challenges can be addressed by using mock data to simulate the behavior of the external service in our tests. The framework is built using Java and utilizes Maven, Lombok, TestNG, Wirework, Cucumber, and Selenide. The architecture follows a classic layered approach. The framework is stored in the same repo as the application to maintain versioning.
@integration
Feature: Data presentation
Scenario Outline: User is able to see cat fact
When [Sys] Cat fact service is mocked
When [UI] User opens main page
Then [UI] The cat fact <catFact> should be shown
Examples: Examples:
| catFact |
| Approximately 40,000 people are bitten by cats in the U.S. annually. |
The key difference from E2E tests is the addition of an extra step, where the external service is stubbed.
@When("[Sys] Cat fact service is mocked")
public void catFactServiceIsMocked() throws IOException {
String json = new String(Files.readAllBytes(Paths.get(mockJsonLocation() + "/catFact.json")));
stubFor(get(urlEqualTo("/fact"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Access-Control-Allow-Origin", "*")
.withBody(json)));
}
To deploy the application locally, you can use the command
npm run start:development // external service is mocked
npm run start:production // external service is NOT mocked
For manual checks and test development, you can utilize Mockoon.
For automated test runs, Wiremock is used as a mock server.
public class MockHooks {
private final static WireMockServer wireMockServer = new WireMockServer();
@BeforeAll
public static void startWiremockServer() {
wireMockServer.start();
}
@AfterAll
public static void stopWiremockServer() {
wireMockServer.stop();
}
}
Jenkins pipeline consists of four steps:
- clone repo
- deploy react application in develop mode(pointed to mock server)
- run integration tests
- generate report
pipeline {
agent any
stages {
stage('Clone repo') {
steps {
git credentialsId: 'git', branch: 'main', url: 'https://gitlab.com/OleksandrPodoliako/cat-fact.git'
}
}
stage('Build and start app') {
steps {
dir('app') {
bat 'npm install'
bat 'start /B cmd /C "npm run start:development | findstr /C:\"Compiled successfully!\""'
}
}
}
stage('Run integration tests') {
steps {
dir('integration-tests') {
bat 'mvn clean test -Pui-integration'
}
}
}
stage('Generate report') {
steps {
dir('integration-tests') {
publishHTML([allowMissing: false, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'target/cucumber-reports', reportFiles: 'cucumber-report.html', reportName: 'Cucumber Report'])
}
}
}
}
}
The full code can be found by the link