Close

JUnit 5 - ExtensionContext - Publishing Reports & Artifacts

[Last Updated: Jan 4, 2026]

The JUnit 5 extension model provides publishing capabilities through the ExtensionContext interface, allowing extensions to publish reports, files, and directories that can be consumed by build tools, IDEs, and CI systems. This enables rich test reporting and artifact generation directly from extensions.

Need for Publishing Capabilities

Extensions often need to share test execution data beyond simple pass/fail results—whether to generate custom reports, export test artifacts, or provide diagnostic information. Without publishing capabilities, extension data would be limited to console output or custom files, missing integration with the broader testing ecosystem.

How Publishing Works in JUnit Ecosystem

The JUnit Platform collects published entries and exposes them to registered listeners. Build tools like Maven Surefire/Gradle convert entries into XML reports and console output. IDEs may display entries in test run details, while CI systems and reporting tools like Allure consume them for enhanced reporting.

ExtensionContext Publishing Methods

Publishing Report Entries

default void publishReportEntry(String key,
                                String value)

Publishes a key-value pair as a report entry. Useful for structured test metadata that build tools and reporting systems can process.

default void publishReportEntry(String value)

Publishes a value-only report entry. Suitable for simple status messages or unstructured information.

default void publishReportEntry(Map<String,
                                String> entries)

Publishes multiple key-value pairs as report entries. Efficient for publishing related metadata in a single operation.

Publishing Files

default void publishFile(String name,
                         MediaType mediaType,
                         ThrowingConsumer<Path> fileWriter)

Publishes a file with specified name and media type. The fileWriter consumer writes content to the provided path. Build tools may attach these files as build artifacts.

Note that ThrowingConsumer is a functional interface in JUnit Jupiter that mirrors the standard Java Consumer but allows the accept method to throw checked exceptions,

Publishing Directories

default void publishDirectory(String name,
                              ThrowingConsumer<Path> directoryWriter)

Publishes a directory with specified name. The directoryWriter consumer creates files within the provided directory path. Useful for publishing multiple related artifacts.

MediaType Constants

JUnit provides standard media type constants in the MediaType class, including TEXT_PLAIN, TEXT_PLAIN_UTF_8, APPLICATION_JSON, APPLICATION_OCTET_STREAM, IMAGE_JPEG, and IMAGE_PNG for common file types.

Example: Test Reporting Extension

This example demonstrates a comprehensive reporting extension that publishes various types of test execution data. The extension generates structured report entries, creates log files with test details, and produces artifact directories with test outputs, showing how extensions can enhance test reporting beyond basic JUnit functionality.

Extension Implementation

package com.logicbig.example;

import org.junit.jupiter.api.MediaType;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

public class TestReportingExtension implements BeforeEachCallback, AfterTestExecutionCallback {

    private static final DateTimeFormatter TIMESTAMP_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

    private static final String START_TIME_KEY = "testStartTime";
    private static final Namespace NAMESPACE =
            Namespace.create(TestReportingExtension.class, "timing");

    @Override
    public void beforeEach(ExtensionContext context) {
        long startTime = System.currentTimeMillis();
        context.getStore(NAMESPACE).put(START_TIME_KEY, startTime);

        // Publish test start information
        context.publishReportEntry("Test Start", LocalDateTime.now().format(TIMESTAMP_FORMATTER));

        Map<String, String> testMetadata = new HashMap<>();
        testMetadata.put("testClass", context.getRequiredTestClass().getSimpleName());
        testMetadata.put("testMethod", context.getRequiredTestMethod().getName());
        testMetadata.put("displayName", context.getDisplayName());

        context.publishReportEntry(testMetadata);
    }

    @Override
    public void afterTestExecution(ExtensionContext context) {

        // Determine test outcome
        String status = context.getExecutionException().isPresent() ? "FAILED" : "PASSED";

        // 1. Publish key-value report entries
        context.publishReportEntry("status", status);
        long testDuration;
        Long startTime = context.getStore(NAMESPACE).get(START_TIME_KEY, Long.class);
        if (startTime != null) {
            testDuration = System.currentTimeMillis() - startTime;
            context.publishReportEntry("duration_ms", String.valueOf(testDuration));
        } else {testDuration = -1;}

        // Clean up
        context.getStore(NAMESPACE).remove(START_TIME_KEY);

        // 2. Publish value-only entry for simple status
        context.publishReportEntry("Test execution " + status.toLowerCase());

        // 3. Publish multiple entries at once
        Map<String, String> executionDetails = new HashMap<>();
        executionDetails.put("timestamp", LocalDateTime.now().format(TIMESTAMP_FORMATTER));
        if (testDuration != -1) {
            executionDetails.put("executionTime", testDuration + " ms");
        }
        executionDetails.put("thread", Thread.currentThread().getName());

        context.getExecutionException().ifPresent(exception -> {
            executionDetails.put("exception", exception.getClass().getSimpleName());
            executionDetails.put("exceptionMessage", exception.getMessage());
        });

        context.publishReportEntry(executionDetails);

        // 4. Publish a detailed log file
        String logFileName = context.getRequiredTestMethod().getName() + "-report.log";
        context.publishFile(
                logFileName,
                MediaType.TEXT_PLAIN,
                path -> Files.writeString(path, "test logs content")
        );

        // 5. Publish an artifact directory with test outputs
        String artifactDirName = context.getRequiredTestMethod().getName() + "-artifacts";
        context.publishDirectory(
                artifactDirName,
                dir -> createArtifacts(dir, context, testDuration, status)
        );
    }

    private void createArtifacts(Path directory,
                                 ExtensionContext context,
                                 long duration,
                                 String status)
            throws IOException {
        Files.createDirectories(directory);

        // Create summary file
        Path summaryFile = directory.resolve("summary.txt");
        Files.writeString(summaryFile,
                          "Test: " + context.getDisplayName() + "\n" +
                                  "Status: " + status + "\n" +
                                  "Generated: " + LocalDateTime.now().format(TIMESTAMP_FORMATTER) + "\n" +
                                  "Artifact count: 3"
        );

        System.out.println("[EXTENSION] Artifact directory created: " + directory.getFileName());
        System.out.println("[EXTENSION] Files created: summary.txt");
    }
}

Report Consumer

Since Maven Surefire does not surface JUnit’s published report entries by default and artifacts in an easily consumable format, we are going to use a TestExecutionListener to capture these events directly from the JUnit Platform. This allows us to observe report metadata and published files without relying on build-tool-specific reporting behavior.

package com.logicbig.example;

import org.junit.platform.engine.reporting.FileEntry;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;

public class MyReportListener implements TestExecutionListener {

    @Override
    public void reportingEntryPublished(TestIdentifier testIdentifier,
                                        ReportEntry entry) {

        System.out.println(
            "[REPORT-ENTRY-LISTENER] " +
            testIdentifier.getDisplayName() +
            " -> " +
            entry.getKeyValuePairs()
        );
    }

    @Override
    public void fileEntryPublished(TestIdentifier testIdentifier,
                                   FileEntry fileEntry) {

        System.out.println(
                "[REPORT-FILE-ENTRY-LISTENER] " +
                        testIdentifier.getDisplayName() +
                        " -> " +
                        fileEntry.toString()
        );
    }
}

Registering the listener

src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener

com.logicbig.example.MyReportListener

Test Class Using the Extension

package com.logicbig.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(TestReportingExtension.class)
public class PerformanceReportingTest {

    @Test
    void performanceTest() {
        System.out.println("[TEST] Executing performance test");
        assertTrue(true);
    }

    @Test
    void memoryUsageTest() {
        System.out.println("[TEST] Executing memory usage test");
        assertTrue(true);

    }
}

Output

$ mvn test -Dtest=PerformanceReportingTest
[INFO] Scanning for projects...
[INFO]
[INFO] ---------< com.logicbig.example:junit-5-extension-publishing >----------
[INFO] Building junit-5-extension-publishing 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-publishing ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-publishing\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-publishing ---
[INFO] No sources to compile
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-publishing ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource from src\test\resources to target\test-classes
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-publishing ---
[INFO] Changes detected - recompiling the module! :source
[INFO] Compiling 3 source files with javac [debug target 17] to target\test-classes
[INFO]
[INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-publishing ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[REPORT-ENTRY-LISTENER] memoryUsageTest() -> {Test Start=2026-01-04 14:29:46.273}
[REPORT-ENTRY-LISTENER] memoryUsageTest() -> {displayName=memoryUsageTest(), testMethod=memoryUsageTest, testClass=PerformanceReportingTest}
[TEST] Executing memory usage test
[REPORT-ENTRY-LISTENER] memoryUsageTest() -> {status=PASSED}
[REPORT-ENTRY-LISTENER] memoryUsageTest() -> {duration_ms=43}
[REPORT-ENTRY-LISTENER] memoryUsageTest() -> {value=Test execution passed}
[REPORT-ENTRY-LISTENER] memoryUsageTest() -> {executionTime=43 ms, thread=main, timestamp=2026-01-04 14:29:46.311}
[REPORT-FILE-ENTRY-LISTENER] memoryUsageTest() -> FileEntry [timestamp = 2026-01-04T14:29:46.354214600, path = D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-publishing\target\junit-jupiter\com.logicbig.example.PerformanceReportingTest\memoryUsageTest()\memoryUsageTest-report.log, mediaType = 'text/plain']
[EXTENSION] Artifact directory created: memoryUsageTest-artifacts
[EXTENSION] Files created: summary.txt
[REPORT-FILE-ENTRY-LISTENER] memoryUsageTest() -> FileEntry [timestamp = 2026-01-04T14:29:46.368595800, path = D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-publishing\target\junit-jupiter\com.logicbig.example.PerformanceReportingTest\memoryUsageTest()\memoryUsageTest-artifacts]
[REPORT-ENTRY-LISTENER] performanceTest() -> {Test Start=2026-01-04 14:29:46.384}
[REPORT-ENTRY-LISTENER] performanceTest() -> {displayName=performanceTest(), testMethod=performanceTest, testClass=PerformanceReportingTest}
[TEST] Executing performance test
[REPORT-ENTRY-LISTENER] performanceTest() -> {status=PASSED}
[REPORT-ENTRY-LISTENER] performanceTest() -> {duration_ms=1}
[REPORT-ENTRY-LISTENER] performanceTest() -> {value=Test execution passed}
[REPORT-ENTRY-LISTENER] performanceTest() -> {executionTime=1 ms, thread=main, timestamp=2026-01-04 14:29:46.386}
[REPORT-FILE-ENTRY-LISTENER] performanceTest() -> FileEntry [timestamp = 2026-01-04T14:29:46.387732600, path = D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-publishing\target\junit-jupiter\com.logicbig.example.PerformanceReportingTest\performanceTest()\performanceTest-report.log, mediaType = 'text/plain']
[EXTENSION] Artifact directory created: performanceTest-artifacts
[EXTENSION] Files created: summary.txt
[REPORT-FILE-ENTRY-LISTENER] performanceTest() -> FileEntry [timestamp = 2026-01-04T14:29:46.388729100, path = D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-publishing\target\junit-jupiter\com.logicbig.example.PerformanceReportingTest\performanceTest()\performanceTest-artifacts]
[INFO] +--com.logicbig.example.PerformanceReportingTest - 0.174 ss
[INFO] | +-- [OK] memoryUsageTest - 0.136 ss
[INFO] | '-- [OK] performanceTest - 0.008 ss
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.343 s
[INFO] Finished at: 2026-01-04T14:29:46+08:00
[INFO] ------------------------------------------------------------------------

The output demonstrates how the TestReportingExtension successfully publishes various types of test execution data. The extension generates structured report entries with test metadata, creates log file and produces artifact directories containing test-specific outputs. These published elements are captured by the JUnit Platform and can be consumed by build tools like Maven Surefire (which converts them to XML reports), IDEs (which display them in test run views), and CI systems (which can attach the files as build artifacts). This shows how extensions can leverage JUnit's publishing API to create rich, integrated test reporting experiences that go beyond simple console output, providing valuable artifacts for debugging, monitoring, and test analysis.

Example Project

Dependencies and Technologies Used:

  • junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
     Version Compatibility: 5.14.0 - 6.0.1Version List
    ×

    Version compatibilities of junit-jupiter-engine with this example:

    • 5.14.0
    • 5.14.1
    • 6.0.0
    • 6.0.1

    Versions in green have been tested.

  • junit-platform-launcher 6.0.1 (Module "junit-platform-launcher" of JUnit)
  • JDK 25
  • Maven 3.9.11

JUnit 5 - Extension Context Publishing Reports & Artifacts Select All Download
  • junit-5-extension-publishing
    • src
      • test
        • java
          • com
            • logicbig
              • example
                • TestReportingExtension.java
          • resources
            • META-INF
              • services

    See Also