Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/junit5-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: keploy-junit5

on:
pull_request:
paths:
- "keploy-junit5/**"
- ".github/workflows/junit5-sdk.yml"
push:
branches: [main]
paths:
- "keploy-junit5/**"
- ".github/workflows/junit5-sdk.yml"
# keploy-junit5-release.yml runs these rows and the e2e lane on a release
# tag before it publishes.
workflow_call:

concurrency:
group: keploy-junit5-${{ github.ref }}
cancel-in-progress: true

jobs:
rows:
name: rows (JDK ${{ matrix.java }}, JUnit ${{ matrix.junit }})
runs-on: ubuntu-latest
strategy:
# One leg failing must not cancel the others: each leg's result is its
# own.
fail-fast: false
matrix:
# The oldest JUnit this extension supports (README.md) on the oldest
# JDK it supports, the last JUnit 5, and JUnit 6 (JDK 17 or later).
include:
- { java: "8", junit: "5.10.5" }
- { java: "17", junit: "5.14.4" }
- { java: "21", junit: "6.1.3" }
defaults:
run:
working-directory: keploy-junit5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
with:
distribution: temurin
java-version: ${{ matrix.java }}
cache: maven
- run: mvn -B -ntp -Djunit.version=${{ matrix.junit }} verify

e2e:
# e2e/lane.sh: this commit's extension, installed with Maven into the
# lane's own Maven repository and used by a small JUnit 5 suite, run with
# Maven under keploy mock record with its provider up, then keploy mock
# replay with the provider stopped; a test whose call has no recorded
# answer, and one that no longer makes its recorded call, fail with
# keploy's text; a quarantined test is skipped with keploy's reason.
name: e2e (keploy mock record and replay)
runs-on: ubuntu-latest
# The lane takes a few minutes, Maven's downloads included: a run past 15
# is a hang.
timeout-minutes: 15
env:
# keploy's enterprise build, which judges each test under
# KEPLOY_PROVEN=1. Pinned, and checked against its digest, so that a
# new keploy release cannot change this job's result unseen.
KEPLOY_VERSION: "3.8.106"
KEPLOY_SHA256: "2c4c9bcf0a703b65827d1cee5ca88fe9d7856844a08cac368b42b83345fef3ce"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
with:
distribution: temurin
java-version: "17"
- name: install keploy ${{ env.KEPLOY_VERSION }}
run: |
curl -fsSL --retry 5 -o "$RUNNER_TEMP/keploy" "https://keploy.io/ent/dl/$KEPLOY_VERSION/enterprise_linux_amd64"
echo "$KEPLOY_SHA256 $RUNNER_TEMP/keploy" | sha256sum -c -
chmod +x "$RUNNER_TEMP/keploy"
- run: KEPLOY_BIN="$RUNNER_TEMP/keploy" bash keploy-junit5/e2e/lane.sh
141 changes: 141 additions & 0 deletions .github/workflows/keploy-junit5-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: keploy-junit5 release

# Publishes io.keploy:keploy-junit5 to Maven Central from a tag.
#
# To release X.Y.Z: push the tag keploy-junit5-vX.Y.Z on a commit of main.
# The rows on every JDK and JUnit leg and the e2e lane (junit5-sdk.yml) run
# on the tagged commit first; the publish runs only when they all pass. It
# sets keploy-junit5/pom.xml's version to X.Y.Z in this run only (the pom on
# main stays a SNAPSHOT), builds the jar with its sources and javadoc jars,
# signs each, and deploys them to Central, which refuses a version it already
# has.
#
# keploy-junit5 is a Maven project of its own, released with its own
# version: release.yml (v*.*.* tags) builds and deploys only the root pom's
# modules, and never this one. This workflow signs and deploys with the same
# key and the same secret names as release.yml: CENTRAL_USERNAME and
# CENTRAL_PASSWORD (a Central Portal user token for the io.keploy namespace),
# MAVEN_GPG_PRIVATE_KEY (the ASCII-armored release key, id 8541784E4EC36FB8)
# and MAVEN_GPG_PASSPHRASE. A step below fails, naming each one that is not
# set, before anything is built or signed.
# Every action this workflow runs, the rows' included, is pinned by commit:
# the publish job holds the signing key and the Central token.
#
# On a pull request the same release build runs with no signing and no
# deploy, so a javadoc error or a missing jar shows before a tag.
on:
push:
tags: ["keploy-junit5-v*"]
pull_request:
paths:
- "keploy-junit5/**"
- ".github/workflows/keploy-junit5-release.yml"
- ".github/workflows/junit5-sdk.yml"

permissions:
contents: read

# Never let two releases of the same tag overlap, and never cancel one
# mid-deploy.
concurrency:
group: keploy-junit5-release-${{ github.ref }}
cancel-in-progress: false

jobs:
rows:
if: github.event_name == 'push'
uses: ./.github/workflows/junit5-sdk.yml

publish:
name: build, sign and publish to Maven Central
if: github.event_name == 'push'
needs: rows
runs-on: ubuntu-latest
# central-publishing-maven-plugin waits until Central reports the
# deployment PUBLISHED (waitMaxTime 7200 s), plus the build ahead of it.
timeout-minutes: 150
env:
# The public key id of keploy's release signing key, as release.yml.
GPG_KEY_ID: 8541784E4EC36FB8
defaults:
run:
shell: bash # -eo pipefail: a failed command before a pipe fails the step
working-directory: keploy-junit5
steps:
- name: The tag names a release version
id: ver
# Before the signing key is imported. The tag name is interpolated
# into Maven's arguments below, so only digits and dots pass.
working-directory: .
run: |
version="${GITHUB_REF_NAME#keploy-junit5-v}"
if [[ "$GITHUB_REF_NAME" != keploy-junit5-v* || ! "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
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"
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: The Central and signing secrets are set
working-directory: .
env:
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
run: |
missing=()
for s in CENTRAL_USERNAME CENTRAL_PASSWORD MAVEN_GPG_PRIVATE_KEY MAVEN_GPG_PASSPHRASE; do
[[ -n "${!s}" ]] || missing+=("$s")
done
if (( ${#missing[@]} )); then
echo "::error::the secret(s) ${missing[*]} are not set; refusing to publish"
exit 1
fi
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Set up JDK 17
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
with:
distribution: temurin
java-version: "17"
# Writes ~/.m2/settings.xml with a `central` server entry read from
# the env vars named here, and imports the signing key.
server-id: central
server-username: CENTRAL_USERNAME
server-password: CENTRAL_PASSWORD
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE
- name: Set the release version in the pom
# Pinned, as release.yml pins it: an unpinned plugin would run
# unreviewed code in the job that holds the signing key.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.21.0:set -DnewVersion="$VERSION" -DgenerateBackupPoms=false
- name: Build, sign and publish
env:
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
run: mvn -B -ntp -P release -DskipTests clean deploy -Dgpg.keyname="$GPG_KEY_ID"

dry-run:
name: release build, dry run
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
defaults:
run:
shell: bash # -eo pipefail: a failed command before a pipe fails the step
working-directory: keploy-junit5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
with:
distribution: temurin
java-version: "17"
- name: Release build, unsigned
run: mvn -B -ntp -P release -DskipTests -Dgpg.skip clean verify
- name: The release carries its jar, sources and javadoc
run: |
v=$(mvn -B -ntp -q help:evaluate -Dexpression=project.version -DforceStdout)
for f in "keploy-junit5-$v.jar" "keploy-junit5-$v-sources.jar" "keploy-junit5-$v-javadoc.jar"; do
test -s "target/$f" || { echo "::error::the release would ship without $f"; exit 1; }
done
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; }
122 changes: 122 additions & 0 deletions keploy-junit5/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# keploy-junit5

A JUnit 5 (Jupiter) extension that joins a test suite to keploy's Universal
Mocker. `keploy mock record` keeps the calls each test's code makes to its
dependencies; `keploy mock replay` answers them from the recording, so the
suite runs with every dependency stopped. keploy judges each test: a call with
no recorded answer, or a recorded call the test no longer makes, fails that
test with keploy's text.

The extension only tells keploy where each test begins and ends; keploy keeps
every rule. It speaks the same wire as go-sdk's `keploytest`,
`@keploy/playwright`, `@keploy/jest` and `keploy-pytest`.

## Use

JUnit 5.10 or later (JUnit 6 included), Java 8 or later. The package is not
published yet: install it from this folder with `mvn install`, then add it to
the test scope:

```xml
<dependency>
<groupId>io.keploy</groupId>
<artifactId>keploy-junit5</artifactId>
<version>0.1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
```

Let JUnit load it for the whole run, in `src/test/resources/junit-platform.properties`:

```properties
junit.jupiter.extensions.autodetection.enabled=true
```

(Gradle: `test { useJUnitPlatform(); systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' }`.)

keploy needs the project's `keploy.yml` to name the recording and CI's test
command; without `mock.command`, keploy refuses the run with
`no keploy project here: set mock.command ...`:

```yaml
# keploy.yml
mock:
name: shop # the recording: keploy/shop/
command: mvn test # CI's test command
```

Then run the suite under keploy with `KEPLOY_PROVEN=1`, in one test JVM.
`--local` keeps the recording on disk, in `keploy/<name>/`:

```sh
KEPLOY_PROVEN=1 keploy mock record --local -c "mvn test"
KEPLOY_PROVEN=1 keploy mock replay --local -c "mvn test"
```

Without keploy (`KEPLOY_MOCK_AGENT` unset or `off`) the extension does
nothing, and the suite runs as plain JUnit.

## What it does under keploy

- The test JVM claims keploy's run once, before the first test class's
`@BeforeAll`, naming keploy's project folder (`KEPLOY_MOCK_PROJECT`).
- Each test begins after its class's `@BeforeAll` methods, before every other
extension's BeforeEach callback and its own `@BeforeEach` methods. It ends
after its `@AfterEach` methods and every other extension's AfterEach
callback, with its outcome: passed, failed, or skipped (a failed
assumption). The calls `@BeforeAll` and `@AfterAll` methods make belong to
the test JVM, outside every test.
- A test keploy set aside is aborted (JUnit's skip) with keploy's reason, and
none of its `@BeforeEach`, test or `@AfterEach` methods runs (other
extensions' AfterEach callbacks do, as JUnit runs them after any BeforeEach
callback stops a test). A line on stderr names it.
- A test keploy failed at its end fails with keploy's text; when it had failed
already, keploy's text is added to its failure.
- Its ID is `<class>::<method>`: the class's binary name
(`com.shop.OrdersTest$Refunds` for a `@Nested` class) and JUnit's name of
the method, with a parameterized or repeated test's invocation index:
`com.shop.OrdersTest::price(java.lang.String)[2]`. Each invocation of a
`@ParameterizedClass` (JUnit 5.13 and later) runs each of the class's tests
as a test of its own, with the class's invocation index before the method:
`com.shop.OrdersTest::[2]price(java.lang.String)`. A test run through a
JUnit Platform `@Suite` has the same ID as when it runs directly.

## Limits

Each of these fails loudly under keploy, so no Jupiter test runs unjudged:

- One test JVM at a time: keploy refuses a second claim. Keep Surefire's
`forkCount` at 1 (its default), and Gradle's `maxParallelForks` at 1.
- JUnit's parallel execution (`junit.jupiter.execution.parallel.enabled=true`)
fails every test class under keploy.
- Registering the extension only with `@ExtendWith`, without autodetection,
fails every test class under keploy: some classes could run without it,
and it must wrap every other extension.
- A `@TestFactory`'s dynamic tests run inside one BeforeEach/AfterEach pair,
so keploy cannot judge them one by one: the factory fails under keploy.
Write them as `@ParameterizedTest`.
- A test begun a second time in one JVM fails (a rerun, such as Surefire's
`rerunFailingTestsCount`, or a second `@Suite` that selects the same test
class): keploy names a test by its class and name.
- A JVM cannot change its own environment, so it does not export
`KEPLOY_TEST_PID` to the processes its tests start; one of them that runs
tests under a keploy SDK claims too, and keploy refuses it.

Only JUnit Jupiter tests are judged. The tests of every other JUnit Platform
engine run unjudged, as without keploy: JUnit 4 tests on the Vintage engine,
and Spock, Kotest, jqwik and Cucumber tests. This extension is a Jupiter
extension, and no other engine calls it; their calls are the test JVM's.

## Releasing

keploy-junit5 is released on its own tag, with its own version, apart from
the repository's `v*.*.*` releases of the agent. To release X.Y.Z, push the
tag `keploy-junit5-vX.Y.Z` on a commit of `main`:
`git tag keploy-junit5-vX.Y.Z <sha> && git push origin keploy-junit5-vX.Y.Z`.
The tag runs the rows and the e2e lane, then
`.github/workflows/keploy-junit5-release.yml` sets the pom's version to X.Y.Z
for that run only, builds the jar with its sources and javadoc jars, signs
each, and deploys them to Maven Central. It uses the repository secrets that
`release.yml` uses: `CENTRAL_USERNAME` and `CENTRAL_PASSWORD` (a Central
Portal user token for the `io.keploy` namespace), `MAVEN_GPG_PRIVATE_KEY` and
`MAVEN_GPG_PASSPHRASE`; it refuses to start, naming each one that is not set.
Loading
Loading