Skip to content

Commit e8ec940

Browse files
authored
feat: keploy-junit5, a JUnit 5 extension for keploy mock record/replay (#188)
* feat: keploy-junit5, a JUnit 5 extension for keploy mock record/replay * test(junit5): run a JUnit suite under keploy mock record and replay * test(junit5): pin the JSON reader's edges and an empty action * fix(junit5): name a test run through a @suite as when it runs directly * fix(junit5): name each @ParameterizedClass invocation's test on its own * ci(junit5): publish keploy-junit5 to Maven Central from its own tag * ci(junit5): pin every action of the junit5 workflows by commit * fix(junit5): give the pom's scm connections their git provider Signed-off-by: slayerjain <shubhamkjain@outlook.com>
1 parent 37cc236 commit e8ec940

25 files changed

Lines changed: 2932 additions & 0 deletions

‎.github/workflows/junit5-sdk.yml‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: keploy-junit5
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "keploy-junit5/**"
7+
- ".github/workflows/junit5-sdk.yml"
8+
push:
9+
branches: [main]
10+
paths:
11+
- "keploy-junit5/**"
12+
- ".github/workflows/junit5-sdk.yml"
13+
# keploy-junit5-release.yml runs these rows and the e2e lane on a release
14+
# tag before it publishes.
15+
workflow_call:
16+
17+
concurrency:
18+
group: keploy-junit5-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
rows:
23+
name: rows (JDK ${{ matrix.java }}, JUnit ${{ matrix.junit }})
24+
runs-on: ubuntu-latest
25+
strategy:
26+
# One leg failing must not cancel the others: each leg's result is its
27+
# own.
28+
fail-fast: false
29+
matrix:
30+
# The oldest JUnit this extension supports (README.md) on the oldest
31+
# JDK it supports, the last JUnit 5, and JUnit 6 (JDK 17 or later).
32+
include:
33+
- { java: "8", junit: "5.10.5" }
34+
- { java: "17", junit: "5.14.4" }
35+
- { java: "21", junit: "6.1.3" }
36+
defaults:
37+
run:
38+
working-directory: keploy-junit5
39+
steps:
40+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
41+
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
42+
with:
43+
distribution: temurin
44+
java-version: ${{ matrix.java }}
45+
cache: maven
46+
- run: mvn -B -ntp -Djunit.version=${{ matrix.junit }} verify
47+
48+
e2e:
49+
# e2e/lane.sh: this commit's extension, installed with Maven into the
50+
# lane's own Maven repository and used by a small JUnit 5 suite, run with
51+
# Maven under keploy mock record with its provider up, then keploy mock
52+
# replay with the provider stopped; a test whose call has no recorded
53+
# answer, and one that no longer makes its recorded call, fail with
54+
# keploy's text; a quarantined test is skipped with keploy's reason.
55+
name: e2e (keploy mock record and replay)
56+
runs-on: ubuntu-latest
57+
# The lane takes a few minutes, Maven's downloads included: a run past 15
58+
# is a hang.
59+
timeout-minutes: 15
60+
env:
61+
# keploy's enterprise build, which judges each test under
62+
# KEPLOY_PROVEN=1. Pinned, and checked against its digest, so that a
63+
# new keploy release cannot change this job's result unseen.
64+
KEPLOY_VERSION: "3.8.106"
65+
KEPLOY_SHA256: "2c4c9bcf0a703b65827d1cee5ca88fe9d7856844a08cac368b42b83345fef3ce"
66+
steps:
67+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
68+
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
69+
with:
70+
distribution: temurin
71+
java-version: "17"
72+
- name: install keploy ${{ env.KEPLOY_VERSION }}
73+
run: |
74+
curl -fsSL --retry 5 -o "$RUNNER_TEMP/keploy" "https://keploy.io/ent/dl/$KEPLOY_VERSION/enterprise_linux_amd64"
75+
echo "$KEPLOY_SHA256 $RUNNER_TEMP/keploy" | sha256sum -c -
76+
chmod +x "$RUNNER_TEMP/keploy"
77+
- run: KEPLOY_BIN="$RUNNER_TEMP/keploy" bash keploy-junit5/e2e/lane.sh
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
name: keploy-junit5 release
2+
3+
# Publishes io.keploy:keploy-junit5 to Maven Central from a tag.
4+
#
5+
# To release X.Y.Z: push the tag keploy-junit5-vX.Y.Z on a commit of main.
6+
# The rows on every JDK and JUnit leg and the e2e lane (junit5-sdk.yml) run
7+
# on the tagged commit first; the publish runs only when they all pass. It
8+
# sets keploy-junit5/pom.xml's version to X.Y.Z in this run only (the pom on
9+
# main stays a SNAPSHOT), builds the jar with its sources and javadoc jars,
10+
# signs each, and deploys them to Central, which refuses a version it already
11+
# has.
12+
#
13+
# keploy-junit5 is a Maven project of its own, released with its own
14+
# version: release.yml (v*.*.* tags) builds and deploys only the root pom's
15+
# modules, and never this one. This workflow signs and deploys with the same
16+
# key and the same secret names as release.yml: CENTRAL_USERNAME and
17+
# CENTRAL_PASSWORD (a Central Portal user token for the io.keploy namespace),
18+
# MAVEN_GPG_PRIVATE_KEY (the ASCII-armored release key, id 8541784E4EC36FB8)
19+
# and MAVEN_GPG_PASSPHRASE. A step below fails, naming each one that is not
20+
# set, before anything is built or signed.
21+
# Every action this workflow runs, the rows' included, is pinned by commit:
22+
# the publish job holds the signing key and the Central token.
23+
#
24+
# On a pull request the same release build runs with no signing and no
25+
# deploy, so a javadoc error or a missing jar shows before a tag.
26+
on:
27+
push:
28+
tags: ["keploy-junit5-v*"]
29+
pull_request:
30+
paths:
31+
- "keploy-junit5/**"
32+
- ".github/workflows/keploy-junit5-release.yml"
33+
- ".github/workflows/junit5-sdk.yml"
34+
35+
permissions:
36+
contents: read
37+
38+
# Never let two releases of the same tag overlap, and never cancel one
39+
# mid-deploy.
40+
concurrency:
41+
group: keploy-junit5-release-${{ github.ref }}
42+
cancel-in-progress: false
43+
44+
jobs:
45+
rows:
46+
if: github.event_name == 'push'
47+
uses: ./.github/workflows/junit5-sdk.yml
48+
49+
publish:
50+
name: build, sign and publish to Maven Central
51+
if: github.event_name == 'push'
52+
needs: rows
53+
runs-on: ubuntu-latest
54+
# central-publishing-maven-plugin waits until Central reports the
55+
# deployment PUBLISHED (waitMaxTime 7200 s), plus the build ahead of it.
56+
timeout-minutes: 150
57+
env:
58+
# The public key id of keploy's release signing key, as release.yml.
59+
GPG_KEY_ID: 8541784E4EC36FB8
60+
defaults:
61+
run:
62+
shell: bash # -eo pipefail: a failed command before a pipe fails the step
63+
working-directory: keploy-junit5
64+
steps:
65+
- name: The tag names a release version
66+
id: ver
67+
# Before the signing key is imported. The tag name is interpolated
68+
# into Maven's arguments below, so only digits and dots pass.
69+
working-directory: .
70+
run: |
71+
version="${GITHUB_REF_NAME#keploy-junit5-v}"
72+
if [[ "$GITHUB_REF_NAME" != keploy-junit5-v* || ! "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
73+
echo "::error::refusing to publish from '$GITHUB_REF_NAME': the tag must be keploy-junit5-vX.Y.Z, X.Y.Z a release version with no leading zeros"
74+
exit 1
75+
fi
76+
echo "version=$version" >> "$GITHUB_OUTPUT"
77+
- name: The Central and signing secrets are set
78+
working-directory: .
79+
env:
80+
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
81+
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
82+
MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
83+
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
84+
run: |
85+
missing=()
86+
for s in CENTRAL_USERNAME CENTRAL_PASSWORD MAVEN_GPG_PRIVATE_KEY MAVEN_GPG_PASSPHRASE; do
87+
[[ -n "${!s}" ]] || missing+=("$s")
88+
done
89+
if (( ${#missing[@]} )); then
90+
echo "::error::the secret(s) ${missing[*]} are not set; refusing to publish"
91+
exit 1
92+
fi
93+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
94+
- name: Set up JDK 17
95+
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
96+
with:
97+
distribution: temurin
98+
java-version: "17"
99+
# Writes ~/.m2/settings.xml with a `central` server entry read from
100+
# the env vars named here, and imports the signing key.
101+
server-id: central
102+
server-username: CENTRAL_USERNAME
103+
server-password: CENTRAL_PASSWORD
104+
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
105+
gpg-passphrase: MAVEN_GPG_PASSPHRASE
106+
- name: Set the release version in the pom
107+
# Pinned, as release.yml pins it: an unpinned plugin would run
108+
# unreviewed code in the job that holds the signing key.
109+
env:
110+
VERSION: ${{ steps.ver.outputs.version }}
111+
run: mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.21.0:set -DnewVersion="$VERSION" -DgenerateBackupPoms=false
112+
- name: Build, sign and publish
113+
env:
114+
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
115+
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
116+
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
117+
run: mvn -B -ntp -P release -DskipTests clean deploy -Dgpg.keyname="$GPG_KEY_ID"
118+
119+
dry-run:
120+
name: release build, dry run
121+
if: github.event_name == 'pull_request'
122+
runs-on: ubuntu-latest
123+
defaults:
124+
run:
125+
shell: bash # -eo pipefail: a failed command before a pipe fails the step
126+
working-directory: keploy-junit5
127+
steps:
128+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
129+
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
130+
with:
131+
distribution: temurin
132+
java-version: "17"
133+
- name: Release build, unsigned
134+
run: mvn -B -ntp -P release -DskipTests -Dgpg.skip clean verify
135+
- name: The release carries its jar, sources and javadoc
136+
run: |
137+
v=$(mvn -B -ntp -q help:evaluate -Dexpression=project.version -DforceStdout)
138+
for f in "keploy-junit5-$v.jar" "keploy-junit5-$v-sources.jar" "keploy-junit5-$v-javadoc.jar"; do
139+
test -s "target/$f" || { echo "::error::the release would ship without $f"; exit 1; }
140+
done
141+
unzip -l "target/keploy-junit5-$v.jar" | grep -q 'io/keploy/junit5/' || { echo "::error::keploy-junit5-$v.jar holds no io/keploy/junit5 classes"; exit 1; }

‎keploy-junit5/README.md‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# keploy-junit5
2+
3+
A JUnit 5 (Jupiter) extension that joins a test suite to keploy's Universal
4+
Mocker. `keploy mock record` keeps the calls each test's code makes to its
5+
dependencies; `keploy mock replay` answers them from the recording, so the
6+
suite runs with every dependency stopped. keploy judges each test: a call with
7+
no recorded answer, or a recorded call the test no longer makes, fails that
8+
test with keploy's text.
9+
10+
The extension only tells keploy where each test begins and ends; keploy keeps
11+
every rule. It speaks the same wire as go-sdk's `keploytest`,
12+
`@keploy/playwright`, `@keploy/jest` and `keploy-pytest`.
13+
14+
## Use
15+
16+
JUnit 5.10 or later (JUnit 6 included), Java 8 or later. The package is not
17+
published yet: install it from this folder with `mvn install`, then add it to
18+
the test scope:
19+
20+
```xml
21+
<dependency>
22+
<groupId>io.keploy</groupId>
23+
<artifactId>keploy-junit5</artifactId>
24+
<version>0.1.0-SNAPSHOT</version>
25+
<scope>test</scope>
26+
</dependency>
27+
```
28+
29+
Let JUnit load it for the whole run, in `src/test/resources/junit-platform.properties`:
30+
31+
```properties
32+
junit.jupiter.extensions.autodetection.enabled=true
33+
```
34+
35+
(Gradle: `test { useJUnitPlatform(); systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' }`.)
36+
37+
keploy needs the project's `keploy.yml` to name the recording and CI's test
38+
command; without `mock.command`, keploy refuses the run with
39+
`no keploy project here: set mock.command ...`:
40+
41+
```yaml
42+
# keploy.yml
43+
mock:
44+
name: shop # the recording: keploy/shop/
45+
command: mvn test # CI's test command
46+
```
47+
48+
Then run the suite under keploy with `KEPLOY_PROVEN=1`, in one test JVM.
49+
`--local` keeps the recording on disk, in `keploy/<name>/`:
50+
51+
```sh
52+
KEPLOY_PROVEN=1 keploy mock record --local -c "mvn test"
53+
KEPLOY_PROVEN=1 keploy mock replay --local -c "mvn test"
54+
```
55+
56+
Without keploy (`KEPLOY_MOCK_AGENT` unset or `off`) the extension does
57+
nothing, and the suite runs as plain JUnit.
58+
59+
## What it does under keploy
60+
61+
- The test JVM claims keploy's run once, before the first test class's
62+
`@BeforeAll`, naming keploy's project folder (`KEPLOY_MOCK_PROJECT`).
63+
- Each test begins after its class's `@BeforeAll` methods, before every other
64+
extension's BeforeEach callback and its own `@BeforeEach` methods. It ends
65+
after its `@AfterEach` methods and every other extension's AfterEach
66+
callback, with its outcome: passed, failed, or skipped (a failed
67+
assumption). The calls `@BeforeAll` and `@AfterAll` methods make belong to
68+
the test JVM, outside every test.
69+
- A test keploy set aside is aborted (JUnit's skip) with keploy's reason, and
70+
none of its `@BeforeEach`, test or `@AfterEach` methods runs (other
71+
extensions' AfterEach callbacks do, as JUnit runs them after any BeforeEach
72+
callback stops a test). A line on stderr names it.
73+
- A test keploy failed at its end fails with keploy's text; when it had failed
74+
already, keploy's text is added to its failure.
75+
- Its ID is `<class>::<method>`: the class's binary name
76+
(`com.shop.OrdersTest$Refunds` for a `@Nested` class) and JUnit's name of
77+
the method, with a parameterized or repeated test's invocation index:
78+
`com.shop.OrdersTest::price(java.lang.String)[2]`. Each invocation of a
79+
`@ParameterizedClass` (JUnit 5.13 and later) runs each of the class's tests
80+
as a test of its own, with the class's invocation index before the method:
81+
`com.shop.OrdersTest::[2]price(java.lang.String)`. A test run through a
82+
JUnit Platform `@Suite` has the same ID as when it runs directly.
83+
84+
## Limits
85+
86+
Each of these fails loudly under keploy, so no Jupiter test runs unjudged:
87+
88+
- One test JVM at a time: keploy refuses a second claim. Keep Surefire's
89+
`forkCount` at 1 (its default), and Gradle's `maxParallelForks` at 1.
90+
- JUnit's parallel execution (`junit.jupiter.execution.parallel.enabled=true`)
91+
fails every test class under keploy.
92+
- Registering the extension only with `@ExtendWith`, without autodetection,
93+
fails every test class under keploy: some classes could run without it,
94+
and it must wrap every other extension.
95+
- A `@TestFactory`'s dynamic tests run inside one BeforeEach/AfterEach pair,
96+
so keploy cannot judge them one by one: the factory fails under keploy.
97+
Write them as `@ParameterizedTest`.
98+
- A test begun a second time in one JVM fails (a rerun, such as Surefire's
99+
`rerunFailingTestsCount`, or a second `@Suite` that selects the same test
100+
class): keploy names a test by its class and name.
101+
- A JVM cannot change its own environment, so it does not export
102+
`KEPLOY_TEST_PID` to the processes its tests start; one of them that runs
103+
tests under a keploy SDK claims too, and keploy refuses it.
104+
105+
Only JUnit Jupiter tests are judged. The tests of every other JUnit Platform
106+
engine run unjudged, as without keploy: JUnit 4 tests on the Vintage engine,
107+
and Spock, Kotest, jqwik and Cucumber tests. This extension is a Jupiter
108+
extension, and no other engine calls it; their calls are the test JVM's.
109+
110+
## Releasing
111+
112+
keploy-junit5 is released on its own tag, with its own version, apart from
113+
the repository's `v*.*.*` releases of the agent. To release X.Y.Z, push the
114+
tag `keploy-junit5-vX.Y.Z` on a commit of `main`:
115+
`git tag keploy-junit5-vX.Y.Z <sha> && git push origin keploy-junit5-vX.Y.Z`.
116+
The tag runs the rows and the e2e lane, then
117+
`.github/workflows/keploy-junit5-release.yml` sets the pom's version to X.Y.Z
118+
for that run only, builds the jar with its sources and javadoc jars, signs
119+
each, and deploys them to Maven Central. It uses the repository secrets that
120+
`release.yml` uses: `CENTRAL_USERNAME` and `CENTRAL_PASSWORD` (a Central
121+
Portal user token for the `io.keploy` namespace), `MAVEN_GPG_PRIVATE_KEY` and
122+
`MAVEN_GPG_PASSPHRASE`; it refuses to start, naming each one that is not set.

0 commit comments

Comments
 (0)