A Gradle plugin and CLI for publishing JUnit test results to a Fern test reporting instance.
This plugin simplifies the process of collecting JUnit XML test reports and publishing them to a Fern test reporting service. It parses JUnit XML reports, converts them to Fern's data model, and sends them to your Fern instance through its API.
To learn more about Fern, check out its repository
- Parse JUnit XML test reports in various formats
- Group test suites and test cases into a cohesive test run
- Track test execution time and status
- Support for test tags
- Configurable verbosity for debugging
Add the plugin to your build.gradle.kts file:
plugins {
id("io.github.guidewire-oss.fern-publisher") version "1.0.0"
}Or in build.gradle:
plugins {
id 'io.github.guidewire-oss.fern-publisher' version '1.0.0'
}Newer versions of the Fern Reporter server require you to pre-register your application to receive a UUID (your project id)
- To register your project send a
POSTrequest to<yourFernUrl>/api/projectwith JSON body of
{
"name": "my-project",
"team_name": "Dev Team",
"comment": "Initial project registration"
}Here is an example curl command for ease of use:
curl -X POST "https://yourFernUrl.com/api/project" \
-H "Content-Type: application/json" \
-d '{
"name": "my-project",
"team_name": "Dev Team",
"comment": "Initial project registration"
}'- You will receive a successful response that looks like:
{
"uuid": "59e06cf8-f390-5093-af2e-3685be593a25",
"name": "my-project",
"team_name": "Dev Team",
"comment": "Initial project registration",
"created_at": "2025-06-06T15:00:28.403029Z",
"updated_at": "2025-06-06T15:00:28.403682Z"
}You will need to take note of the returned ProjectID UUID for use in configuring the plugin, as described below
Configure the plugin in your build script:
fernPublisher {
fernUrl.set("https://your-fern-instance.example.com")
projectId.set("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
projectName.set("my-project")
reportPaths.set(listOf("build/test-results/test/*.xml"))
fernTags.set(listOf("automated", "integration"))
verbose.set(false)
failOnError.set(false)
}| Property | Description | Required | Default |
|---|---|---|---|
| fernUrl | URL of your Fern instance | Yes | - |
| projectId | The id of your project supplied by registering a project in Fern. | No* | "" |
| projectName | Name of your project in Fern | No* | "" |
| * NOTE: you must include a projectName or a projectID (or both). Newer versions of the Fern Reporting server require you to pre-register your project to obtain an ID | |||
| reportPaths | Glob patterns for locating JUnit XML reports | Yes | - |
| fernTags | Tags to apply to all tests | No | empty list |
| verbose | Enable verbose logging | No | false |
| failOnError | When true, will fail when error is thrown. Errors will always log. | No | false |
If your Fern instance requires OAuth authentication, you can configure it using environment variables:
export AUTH_URL="https://auth.example.com/token"
export FERN_AUTH_CLIENT_ID="your-client-id"
export FERN_AUTH_CLIENT_SECRET="your-client-secret"
export FERN_CLIENT_SCOPE="fern.write fernproject.myproject"| Variable | Description | Required |
|---|---|---|
| AUTH_URL | OAuth token endpoint URL. Setting this enables OAuth. | Yes (to enable auth) |
| FERN_AUTH_CLIENT_ID | OAuth client identifier | Yes (if AUTH_URL is set) |
| FERN_AUTH_CLIENT_SECRET | OAuth client secret | Yes (if AUTH_URL is set) |
| FERN_CLIENT_SCOPE | Space-separated OAuth scopes (e.g., fern.write fern.read) |
No |
OAuth is enabled when AUTH_URL is set; if it is set, FERN_AUTH_CLIENT_ID and
FERN_AUTH_CLIENT_SECRET are required and the plugin will fail fast if either is missing.
The plugin uses the OAuth 2.0 client credentials flow and adds the bearer token to API calls.
Alternatively, you can configure OAuth in your build.gradle via the fernPublisher extension.
Prefer sourcing secrets from the environment rather than hardcoding them:
fernPublisher {
authUrl = System.getenv("AUTH_URL")
authClientId = System.getenv("FERN_AUTH_CLIENT_ID")
authClientSecret = System.getenv("FERN_AUTH_CLIENT_SECRET")
authScopes = "fern.write fernproject.myproject"
}If authUrl is set in the extension it takes precedence; the environment variables above are
used as a fallback when authUrl is left blank.
Run the task to publish test results:
./gradlew publishToFern
You can also make it run automatically after your tests:
tasks.named("test") {
finalizedBy("publishToFern")
}fernPublisher {
fernUrl.set("https://fern.example.com")
projectName.set("backend-api")
reportPaths.set(listOf("build/test-results/test/*.xml"))
}fernPublisher {
fernUrl.set("https://fern.example.com")
projectName.set("full-stack")
reportPaths.set(
listOf(
"${project.buildDir}/test-results/test/*.xml",
"frontend/build/test-results/test/*.xml"
)
)
fernTags.set(listOf("ci", "nightly"))
}- Gradle 6.1 or later
- Kotlin 1.4 or later
- JUnit 4 and JUnit 5 XML report formats
This project also provides a CLI tool, fern-junit-client, for collecting and publishing JUnit XML test reports to a Fern Reporter instance. It is designed to work the same way as the Gradle plugin, but can be used independently of Gradle and Java.
Builds of the CLI binary are available for Linux, macOS, and Windows on the Releases page.
fern-junit-client send \
--fern-url <FERN_URL> \
--project-name <PROJECT_NAME> \
--project-id <PROJECT_ID> \
--file-pattern <REPORT_PATTERN> \
[--tags <TAGS>] \
[--verbose]--fern-url(-u): Base URL of the Fern Reporter instance to send test reports to (required)--project-name(-n): Name of the project to associate test reports with (required)--project-id(-i): Project ID to associate test reports with (required)--file-pattern(-f): Glob pattern(s) for JUnit XML reports (can be repeated, required)--tags(-t): (Optional) Comma-separated tags to include on the test run--verbose(-v): (Optional) Enable verbose output for debugging
fern-junit-client send \
--fern-url https://fern.example.com \
--project-name my-service \
--project-id 1234 \
--file-pattern "build/test-results/**/*.xml" \
--tags "ci,nightly" \
--verbosegit clone https://github.com/your-org/fern-junit-publisher.git
cd fern-junit-publisher
./gradlew buildTo test the plugin locally during development, you can publish it to your local Maven repository (.m2).
Important: You must specify a version number when publishing. Use the latest released version or the next version you plan to release.
./gradlew publishToMavenLocal -Pversion=1.1.0The plugin will be published to your local .m2 repository at:
~/.m2/repository/io/github/guidewire-oss/fern-junit-gradle-plugin/1.1.0/
To use your locally published plugin in a test project:
- Configure your test project's
settings.gradle(orsettings.gradle.kts) to usemavenLocal():
pluginManagement {
repositories {
mavenLocal() // Check local repository first
mavenCentral()
}
}- Apply the plugin in your test project's
build.gradle:
plugins {
id 'io.github.guidewire-oss.fern-publisher' version '1.1.0'
}- Configure and test the plugin as described in the Configuration section.
Native CLI binaries are built using GraalVM's native-image tool. This allows the CLI to run without requiring a Java runtime, making it lightweight and portable.
For building the CLI, you will need to have GraalVM installed and set the environment variable GRAALVM_HOME as your GraalVM install location.
Once that is set up, run the following command in the project root:
./gradlew nativeImage -Pversion="1.1.0"This project uses JUnit 5 (Jupiter) for testing along with AssertJ for assertions, WireMock for HTTP mocking, and Gradle TestKit for plugin testing.
Run all tests:
./gradlew testRun tests with verbose output:
./gradlew test --infoRun a specific test class:
./gradlew test --tests TestParserTest
./gradlew test --tests DataSenderTest
./gradlew test --tests FernPublisherPluginTestRun a specific test method:
./gradlew test --tests "TestParserTest.parseReports should handle empty file pattern"Continuous testing (watches for changes):
./gradlew test --continuousClean and test:
./gradlew clean testThe project includes three main test suites located in src/test/kotlin/:
-
TestParserTest.kt - Tests JUnit XML parsing logic, including:
- Parsing various JUnit XML formats
- Handling empty file patterns
- Timezone handling
- Test suite and test case extraction
-
DataSenderTest.kt - Tests HTTP client and API communication, including:
- Sending test data to Fern Reporter API
- HTTP retry logic
- Error handling and redirects
-
FernPublisherPluginTest.kt - Tests Gradle plugin functionality, including:
- Plugin configuration
- Task registration
- Integration with Gradle build lifecycle
- JUnit Jupiter (JUnit 5) - Testing framework
- AssertJ - Fluent assertion library
- WireMock - HTTP service mocking
- Gradle TestKit - Gradle plugin testing framework