1. Abstract
RESTEasy JUnit Extension is a JUnit extension that provides seamless integration for testing Jakarta REST applications
using SeBootstrap. It manages the Jakarta REST server lifecycle, provides resource injection, and offers a clean API
for writing REST API tests.
2. Quick Start
2.1. Maven Dependency
Add the extension to your test dependencies:
<dependency>
<groupId>dev.resteasy.junit</groupId>
<artifactId>resteasy-junit-extension</artifactId>
<version>{project.version}</version>
<scope>test</scope>
</dependency>
<!-- Jakarta REST implementation (e.g., RESTEasy) -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-undertow-cdi</artifactId>
<version>${version.resteasy}</version>
<scope>test</scope>
</dependency>
2.2. Basic Example
2.2.1. Simple Resource-Based Test
For simple tests, you can specify resource classes directly:
@RestBootstrap(HelloResource.class)
public class HelloResourceTest {
@RestResource
private Client client;
@RestResource
private URI baseUri;
@Test
public void testHello() {
Response response = client.target(baseUri)
.path("/hello")
.request()
.get();
assertEquals(200, response.getStatus());
assertEquals("Hello, World!", response.readEntity(String.class));
}
}
2.2.2. Application-Based Test
For more complex scenarios, use a custom Application class:
@RestBootstrap(MyApplication.class)
public class HelloResourceTest {
@RestResource
private Client client;
@RestResource
private URI baseUri;
@Test
public void testHello() {
Response response = client.target(baseUri)
.path("/hello")
.request()
.get();
assertEquals(200, response.getStatus());
assertEquals("Hello, World!", response.readEntity(String.class));
}
}
That’s it! The extension automatically:
-
Starts a
SeBootstrapinstance with your resources or Application -
Injects a configured REST client
-
Provides the server’s base URI
-
Shuts everything down after tests complete
3. Core Concepts
3.1. @RestBootstrap Annotation
The @RestBootstrap annotation marks a test class to start a Jakarta REST SeBootstrap instance.
There are two ways to specify what to bootstrap:
3.1.1. Option 1: Resource Classes (Simple Cases)
For simple tests, specify Jakarta REST resource classes directly:
@RestBootstrap({UserResource.class, OrderResource.class})
public class MyTest {
// Tests run with a synthetic Application containing these resources
}
This is ideal when you only need to test specific resources without custom Application configuration.
3.1.2. Option 2: Application Class (Advanced Cases)
For more control, specify a custom Application class:
@RestBootstrap(application = MyApplication.class)
public class MyTest {
// Tests run with MyApplication
}
Use this when you need:
-
Custom
@ApplicationPathconfiguration -
Provider registration (filters, interceptors, exception mappers)
-
Custom application properties
-
Programmatic configuration in the Application class
3.1.3. Mutual Exclusivity
Important: Exactly one of value() or application() must be specified. Specifying both or neither will result in an error.
Attributes:
-
application- The Jakarta RESTApplicationclass to bootstrap (mutually exclusive withvalue) -
value- Jakarta REST resource classes to bootstrap (mutually exclusive withapplication) -
configFactory- OptionalConfigurationProviderfor custom server configuration -
sslClientAuthentication- SSL client authentication mode when using@SelfSignedCert(default:OPTIONAL). See SSL/TLS Testing. -
timeout- Startup/shutdown timeout (default: 60 seconds) -
timeoutUnit- Timeout time unit (default:SECONDS)
3.2. Resource Injection
The extension can inject several types into your tests:
Type |
Description |
Injection Points |
|
REST client for making HTTP requests |
Fields, Parameters |
|
WebTarget, optionally qualified with |
Fields, Parameters |
|
Server base URI, optionally qualified with |
Fields, Parameters |
|
URI builder for constructing URIs (mutable, always fresh) |
Parameters only |
|
The SeBootstrap configuration |
Fields, Parameters |
|
Self-signed certificate artifacts (requires |
Fields, Parameters |
UriBuilder is only available for method/constructor parameters and does not require the @RestResource annotation. Due to its mutable nature, a fresh instance is created for each parameter.
|
Injection Points:
-
Static fields
-
Instance fields
-
Constructor parameters
-
Method parameters (
@BeforeAll,@BeforeEach,@AfterEach,@AfterAll,@Test)
@RestBootstrap(MyApp.class)
public class InjectionExamplesTest {
// Static field injection
@RestResource
private static Client staticClient;
// Instance field injection
@RestResource
private URI baseUri;
// Constructor injection
public InjectionExamplesTest(@RestResource Client client) {
// ...
}
// Lifecycle method injection
@BeforeEach
public void setUp(@RestResource WebTarget target) {
// ...
}
// Test method injection
@Test
public void testSomething(@RestResource Client client,
@RestResource URI uri,
UriBuilder builder) { // No @RestResource needed
// ...
}
}
4. Advanced Features
4.1. Path Qualifiers with @RequestPath
Use @RequestPath to inject URIs or WebTargets pointing to specific paths:
@RestBootstrap(MyApp.class)
public class PathQualifiersTest {
@RestResource
@RequestPath("/users")
private WebTarget usersTarget;
@RestResource
@RequestPath("/orders")
private URI ordersUri;
@Test
public void testUsers() {
// usersTarget already points to /users
Response response = usersTarget.request().get();
assertEquals(200, response.getStatus());
}
@Test
public void testOrders(@RestResource @RequestPath("/orders/123") URI orderUri) {
// orderUri points to /orders/123
Response response = client.target(orderUri).request().get();
assertEquals(200, response.getStatus());
}
}
Path Formats:
-
Both
/pathandpathwork (leading slash is optional) -
Nested paths work:
/api/v1/users
4.2. Custom Client Configuration
Use @RestClientConfig to provide custom client configuration:
public class CustomTimeoutProvider implements RestClientBuilderProvider {
@Override
public ClientBuilder getClientBuilder() {
return ClientBuilder.newBuilder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS);
}
}
@RestBootstrap(MyApp.class)
public class CustomClientTest {
@RestResource
@RestClientConfig(CustomTimeoutProvider.class)
private Client customClient;
@RestResource
private Client defaultClient; // Uses default configuration
}
4.2.1. Registering Providers Inline
For simple cases — registering a filter, interceptor, or message body reader/writer — you can list the provider classes
directly on the annotation instead of implementing a RestClientBuilderProvider:
@RestBootstrap(MyApp.class)
public class ProviderTest {
@RestResource
@RestClientConfig(providers = { LoggingFilter.class, AuthFilter.class })
private Client client;
}
Each provider is registered via ClientBuilder.register(Class), so it must have a public, no-argument constructor. A
provider that requires constructor arguments or additional setup should be registered through a
RestClientBuilderProvider instead.
Providers listed here are registered after the value() provider (if one is specified) has configured the builder, so
a shared configuration provider can be combined with one or more providers specified at the injection point:
@RestResource
@RestClientConfig(value = CustomTimeoutProvider.class, providers = LoggingFilter.class)
private Client client;
4.3. Custom Server Configuration
4.3.1. Using Configuration Parameters
For simple configuration overrides, use JUnit configuration parameters (system properties or junit-platform.properties):
# Via system properties (useful for CI/avoiding port conflicts)
mvn test -Ddev.resteasy.junit.extension.port=8085
mvn test -Ddev.resteasy.junit.extension.host=0.0.0.0
# Or via junit-platform.properties
dev.resteasy.junit.extension.protocol=http
dev.resteasy.junit.extension.host=localhost
dev.resteasy.junit.extension.port=8080
dev.resteasy.junit.extension.root-path=/api
Supported Parameters:
-
dev.resteasy.junit.extension.protocol- Protocol (http/https) -
dev.resteasy.junit.extension.host- Server host -
dev.resteasy.junit.extension.port- Server port -
dev.resteasy.junit.extension.root-path- Application root path
Note: Configuration parameters are only used when no explicit configFactory is specified on @RestBootstrap.
4.3.2. Using ConfigurationProvider
For more complex configuration, implement ConfigurationProvider:
public class CustomPortProvider implements ConfigurationProvider {
@Override
public SeBootstrap.Configuration getConfiguration(ExtensionContext context) {
return SeBootstrap.Configuration.builder()
.port(9090)
.host("localhost")
.build();
}
}
@RestBootstrap(value = MyApp.class, configFactory = CustomPortProvider.class)
public class CustomPortTest {
// Tests run on port 9090
// Configuration parameters are ignored when configFactory is specified
}
4.4. SSL/TLS Testing
The extension provides built-in support for SSL/TLS testing with self-signed certificates. No external certificate setup or keystore configuration is required.
4.4.1. Quick Start
Add @SelfSignedCert alongside @RestBootstrap to start the server on HTTPS with auto-configured SSL:
@SelfSignedCert
@RestBootstrap(MyResource.class)
public class SslTest {
@RestResource
private Client client; // auto-configured with client SSL context
@RestResource
private URI baseUri;
@Test
public void testHttps() {
assertEquals("https", baseUri.getScheme());
Response response = client.target(baseUri)
.path("/hello")
.request()
.get();
assertEquals(200, response.getStatus());
}
}
When @SelfSignedCert is present alongside @RestBootstrap, the extension automatically:
-
Generates self-signed server and client certificates using the JDK
keytool -
Starts the
SeBootstrapinstance with HTTPS and the server SSL context -
Configures injected
ClientandWebTargetinstances with the client SSL context -
Cleans up all generated keystore files after the test run
-
Tags the test class with
ssl-testfor JUnit tag filtering
Use the ssl-test tag to include or exclude SSL tests. For example, with Maven Surefire:
mvn test -Dgroups=ssl-test (run only SSL tests) or mvn test -DexcludedGroups=ssl-test (skip SSL tests).
|
4.4.2. Accessing Certificate Artifacts
For advanced scenarios — such as configuring a non-Jakarta REST HTTP client or verifying certificate details — inject the
SelfSignedCertificate using @SslCert:
@SelfSignedCert
@RestBootstrap(MyResource.class)
public class SslTest {
@SslCert
private SelfSignedCertificate certificate;
@RestResource
@RequestPath("hello")
private URI uri;
@Test
public void testWithHttpClient() throws Exception {
HttpClient httpClient = HttpClient.newBuilder()
.sslContext(certificate.clientSslContext())
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
}
@Test
public void testWithKeyStoreApi() {
try (Client client = ClientBuilder.newBuilder()
.keyStore(certificate.clientKeyStore(),
SelfSignedCertificate.KEYSTORE_PASSWORD)
.trustStore(certificate.clientTrustStore())
.build()) {
// Use client...
}
}
}
The SelfSignedCertificate interface provides:
Method |
Description |
|
Pre-configured |
|
|
|
|
|
|
|
|
|
The password used for all generated stores |
|
The keystore type (e.g., |
4.4.3. Standalone Certificate Usage
@SelfSignedCert can be used independently of @RestBootstrap when only certificate artifacts are needed:
@SelfSignedCert
public class StandaloneCertTest {
@SslCert
private SelfSignedCertificate certificate;
@Test
public void testCertificate() {
assertNotNull(certificate.serverSslContext());
assertNotNull(certificate.clientSslContext());
assertTrue(Files.exists(certificate.serverKeyStorePath()));
}
}
4.4.4. SSL Client Authentication
The sslClientAuthentication attribute on @RestBootstrap controls whether the server requires client certificates
during the TLS handshake:
@SelfSignedCert
@RestBootstrap(value = MyResource.class,
sslClientAuthentication = SSLClientAuthentication.MANDATORY)
public class MutualTlsTest {
@RestResource
private Client client; // configured with full client cert
@RestResource
private URI baseUri;
@Test
public void clientWithCertSucceeds() {
// Injected client has client SSL context — succeeds
Response response = client.target(baseUri)
.path("/hello")
.request()
.get();
assertEquals(200, response.getStatus());
}
}
Available modes:
-
SSLClientAuthentication.OPTIONAL(default) — Server requests a client certificate but does not require one. This is the recommended default since the injected client is always configured with the client SSL context. -
SSLClientAuthentication.MANDATORY— Server requires a valid client certificate. Connections without one are rejected. -
SSLClientAuthentication.NONE— Server does not request a client certificate.
The sslClientAuthentication attribute is only used by the default ConfigurationProvider. Custom providers
specified via configFactory are responsible for their own SSL client authentication configuration.
|
4.4.5. Custom SSL Configuration
For full control over SSL configuration, use @SslCert field injection in custom providers:
Custom Server Configuration:
public class CustomSslConfigProvider implements ConfigurationProvider {
@SslCert
private SelfSignedCertificate certificate;
@Override
public SeBootstrap.Configuration getConfiguration(ExtensionContext context) {
return SeBootstrap.Configuration.builder()
.protocol("HTTPS")
.sslContext(certificate.serverSslContext())
.port(9443)
.build();
}
}
@SelfSignedCert
@RestBootstrap(value = MyResource.class,
configFactory = CustomSslConfigProvider.class)
public class CustomSslTest {
// Server runs on HTTPS port 9443 with custom configuration
}
Custom Client Configuration:
public class CustomSslClientProvider implements RestClientBuilderProvider {
@SslCert
private SelfSignedCertificate certificate;
@Override
public ClientBuilder getClientBuilder() {
return ClientBuilder.newBuilder()
.sslContext(certificate.clientSslContext())
.connectTimeout(5, TimeUnit.SECONDS);
}
}
@SelfSignedCert
@RestBootstrap(MyResource.class)
public class CustomSslClientTest {
@RestResource
@RestClientConfig(CustomSslClientProvider.class)
private Client sslClient; // custom SSL + timeout config
}
4.5. Nested Test Classes
Nested test classes can inherit or override the parent’s @RestBootstrap:
@RestBootstrap(MyApp.class)
public class OuterTest {
@RestResource
private static Client outerClient;
@Test
public void testOuter() {
// Uses MyApp on default port
}
@Nested
class InheritedTests {
// Inherits parent's @RestBootstrap - shares same instance
@Test
public void testInherited(@RestResource Client client) {
// Same client as outerClient
}
}
@Nested
@RestBootstrap(value = MyApp.class, configFactory = SecondInstanceConfigurationProvider.class)
class IsolatedTests {
// Gets its own SeBootstrap instance with different config
@Test
public void testIsolated(@RestResource Client client) {
// Different client than outerClient
}
}
}
5. Extension Points
5.1. Custom Resource Producers
Implement RestResourceProducer to inject custom types:
public class DataSourceProducer implements RestResourceProducer {
@Override
public boolean canInject(ExtensionContext context, Class<?> clazz,
Annotation... qualifiers) {
return DataSource.class.isAssignableFrom(clazz);
}
@Override
public Object produce(ExtensionContext context, Class<?> clazz,
Annotation... qualifiers) {
// Create and return your DataSource
return createDataSource();
}
}
Registration:
Create META-INF/services/dev.resteasy.junit.extension.api.RestResourceProducer:
com.example.DataSourceProducer
Usage:
@RestBootstrap(MyApp.class)
public class DatabaseTest {
@RestResource
private DataSource dataSource; // Injected by custom producer
@Test
public void testDatabase() {
// Use dataSource...
}
}
5.2. Resource Scoping
Custom RestResourceProducer implementations can control the lifecycle scope of injected resources by overriding the
scope() method. This determines when AutoCloseable resources are automatically closed.
Available Scopes:
-
Scope.DEFAULT(default) - Uses the natural scope of the injection point:-
Fields (static and instance): Cleaned up when the test class completes
-
Method parameters: Cleaned up when the test method completes
-
-
Scope.CLASS- Always use class scope:-
All injections (fields and parameters): Cleaned up when the test class completes
-
-
Scope.NEW- Create a fresh instance for every injection:-
No caching -
produce()is called for every field and parameter injection -
Each injection point gets its own isolated instance
-
When to use each scope:
Use Scope.CLASS for:
-
Expensive-to-create resources (REST clients, database connection pools)
-
Thread-safe, immutable, or stateless resources
-
Resources designed for reuse across multiple test methods
Use Scope.DEFAULT for:
-
Resources that need isolation between test methods (temporary files, test-specific data)
-
Resources that accumulate state or side effects
-
Lightweight resources where recreation cost is negligible
Use Scope.NEW for:
-
Mutable resources that must be isolated per injection (WebTarget, builders)
-
Resources that maintain internal state and cannot be safely shared
-
Resources where each usage context requires a fresh instance
Example: Temporary File Producer with Method-Scoped Cleanup
public class TempFileProducer implements RestResourceProducer {
@Override
public boolean canInject(ExtensionContext context, Class<?> clazz,
Annotation... qualifiers) {
return TempFile.class.isAssignableFrom(clazz);
}
@Override
public Object produce(ExtensionContext context, Class<?> clazz,
Annotation... qualifiers) {
return new TempFile(); // AutoCloseable - deletes file on close
}
@Override
public Scope scope() {
return Scope.DEFAULT; // Method parameters auto-cleanup per method
}
}
@RestBootstrap(MyApp.class)
public class FileProcessingTest {
@Test
public void testMethod1(@RestResource TempFile tempFile) {
// Fresh temp file created for this method
tempFile.write("test data");
// Automatically deleted after this method completes
}
@Test
public void testMethod2(@RestResource TempFile tempFile) {
// Different temp file from testMethod1
// Also deleted after this method completes
}
}
Built-in Producer Scopes:
Built-in producers use the following scopes:
-
Client,URI,Configuration- UseScope.CLASS(shared for performance, thread-safe) -
WebTarget- UsesScope.NEW(mutable, requires isolation per injection) -
UriBuilder- Always creates fresh instances (mutable)
5.3. Global Configuration Providers
Register a global ConfigurationProvider or RestClientBuilderProvider via ServiceLoader to apply configuration to all
tests without specifying it on each annotation.
Create META-INF/services/dev.resteasy.junit.extension.api.ConfigurationProvider:
com.example.GlobalPortProvider
Now all tests without an explicit configFactory will use GlobalPortProvider.
Configuration Precedence:
When determining which configuration to use, the extension follows this precedence (highest to lowest):
-
Explicit
configFactoryon@RestBootstrapannotation - full control, ignores all other configuration -
Global
ConfigurationProviderregistered via ServiceLoader -
Configuration parameters (
dev.resteasy.junit.extension.*properties) -
SeBootstrap defaults (random port, localhost, http, root path /)
5.4. Choosing Between value() and application()
Here’s a comparison to help you decide which approach to use:
Use value() when:
// Simple test with just resource classes
@RestBootstrap({UserResource.class, OrderResource.class})
public class SimpleApiTest {
@RestResource
private WebTarget target;
@Test
public void testGetUser() {
Response response = target.path("/users/123").request().get();
assertEquals(200, response.getStatus());
}
}
Use application() when you need more control:
@ApplicationPath("/api/v1")
public class MyApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return Set.of(UserResource.class, OrderResource.class, CustomExceptionMapper.class);
}
@Override
public Map<String, Object> getProperties() {
// Configure application properties
return Map.of("my.custom.property", "value");
}
}
@RestBootstrap(application = MyApplication.class)
public class AdvancedApiTest {
@RestResource
@RequestPath("/api/app/")
private WebTarget target;
@Test
public void testGetUser() {
Response response = target.path("/users/123").request().get();
assertEquals(200, response.getStatus());
}
}
6. Examples
Complete working examples can be found in the test suite. The test suite demonstrates:
-
ClientInstanceTest- Client injection and instance scoping across all injection points -
SeBootstrapTest- Basic SeBootstrap usage with various injection types -
WebTargetTest- WebTarget with@RequestPathin different formats -
UriInjectionTest- URI injection with and without@RequestPath -
UriBuilderTest- UriBuilder injection and mutability -
SharedConfigInstanceTest- Configuration injection and nested test behavior -
CustomRestResourceProducerTest- CustomRestResourceProducerimplementation -
ResourceScopeTest- Resource lifecycle scoping (CLASS vs DEFAULT) -
MethodScopedResourceTest- Practical example of method-scoped temporary files -
SslBootstrapTest-@SelfSignedCertwith@RestBootstrapfor HTTPS testing -
SelfSignedCertStandaloneTest- Standalone certificate usage without@RestBootstrap -
SslClientAuthenticationTest-SSLClientAuthenticationmodes (OPTIONAL, MANDATORY, NONE) -
SslCustomConfigProviderTest- CustomConfigurationProviderwith@SslCertinjection -
SslCustomClientProviderTest- CustomRestClientBuilderProviderwith@SslCertinjection
7. Configuration Reference
7.1. Configuration Parameters
The extension supports the following JUnit configuration parameters. These can be set via system properties (-D flags) or in junit-platform.properties:
Parameter |
Description |
Default |
|
Protocol (http/https) |
http |
|
Server host |
localhost |
|
Server port (integer) |
Random available port |
|
Application root path |
/ |
Usage examples:
# Command line
mvn test -Ddev.resteasy.junit.extension.port=8085
# CI environment avoiding port conflicts
mvn verify -Ddev.resteasy.junit.extension.port=9999 -Ddev.resteasy.junit.extension.host=0.0.0.0
# src/test/resources/junit-platform.properties
dev.resteasy.junit.extension.port=8080
dev.resteasy.junit.extension.root-path=/api/v1
Note: Configuration parameters are ignored when an explicit configFactory is specified on @RestBootstrap.
7.2. SeBootstrap Configuration API
For programmatic configuration, implement ConfigurationProvider. All SeBootstrap.Configuration builder options are available.
Refer to the ConfigurationProvider JavaDoc for implementation details.
8. Frequently Asked Questions
Q: Can I use this with any Jakarta REST implementation?
A: Yes! The extension uses standard Jakarta REST 3.1+ SeBootstrap. Any compliant implementation (RESTEasy, Jersey, etc.) that supports SeBootstrap will work.
Q: How do I test CDI-enabled applications?
A: Use a Jakarta REST implementation that supports CDI, like resteasy-undertow-cdi. The extension uses @RestResource instead of @jakarta.inject.Inject to avoid conflicts with CDI in your application code.
Q: Can I run tests in parallel?
A: Yes, but each test class gets its own SeBootstrap instance. Parallel execution of methods within a single class is not recommended since they share the same server instance.
Q: How do I enable debug logging?
A: Set the dev.resteasy.junit.extension logger to DEBUG level in your logging framework configuration.
Q: Why am I getting "No SeBootstrap instance available"?
A: Ensure your test class is annotated with @RestBootstrap and that you have a Jakarta REST implementation with SeBootstrap support on your test classpath.
Q: Can nested test classes have different configurations?
A: Yes! Add @RestBootstrap with a custom configFactory to the nested class. However, note that some Jakarta REST implementations (like RESTEasy with CDI) may not support running multiple SeBootstrap instances simultaneously due to singleton container limitations.
Q: How do I test with HTTPS/SSL?
A: Add @SelfSignedCert alongside @RestBootstrap on your test class. The extension generates self-signed certificates and configures HTTPS automatically. See SSL/TLS Testing for details and examples.
Q: Can I use my own certificates instead of auto-generated ones?
A: Yes. Implement a custom ConfigurationProvider (for the server) and/or RestClientBuilderProvider (for the client) with your own SSL configuration. You do not need @SelfSignedCert when providing your own certificates.
Q: Are self-signed certificates shared across test classes?
A: Yes. Certificates are generated once per JVM and stored in the root extension context. All test classes annotated
with @SelfSignedCert share the same certificate artifacts. This avoids the overhead of running keytool for every
test class.
Q: When are injected resources cleaned up?
A: It depends on the producer’s scope:
-
Built-in resources (
Client,URI,Configuration): UseCLASSscope - cleaned up when the test class completes, regardless of injection point. -
Built-in
WebTarget: UsesNEWscope - each injection creates a fresh instance. IfAutoCloseable, cleaned up based on the injection point (field vs parameter). -
Custom producers with
DEFAULTscope: Fields are cleaned up when the test class completes. Method parameters are cleaned up when the test method completes. -
Custom producers with
CLASSscope: Always cleaned up when the test class completes. -
Custom producers with
NEWscope: Each injection is independent - never cached or reused.
Override scope() in your custom RestResourceProducer to control this behavior.
9. Related Documentation
-
Javadoc API Documentation - Complete API reference
-
Jakarta REST Specification - Jakarta REST specification
-
RESTEasy Documentation - RESTEasy user guide
-
JUnit User Guide - JUnit documentation
10. License
This project is licensed under the Apache License v2.0. See the LICENSE file for details.