Creating a KTestify Plugin
This guide walks you through creating a ktestify plugin from start to finish using the ktestify-plugin-archetype Maven archetype. The archetype generates a complete, compilable project with all the boilerplate: plugin SPI class, config reader, transport layer, Cucumber step definitions, services, unit tests, and build configuration.
The archetype is the recommended way to start a new plugin. It ensures your project follows the correct structure, naming conventions, and build setup from day one.
How the Archetype Works
The ktestify plugin archetype is a standard Maven archetype that uses Velocity templating to generate a project tailored to your plugin. When you run mvn archetype:generate, Maven prompts you for a set of parameters (plugin name, author, GitHub repo, etc.) and produces a complete project directory.
What gets generated
ktestify-plugin-myplugin/
├── pom.xml ← Full POM with Spotless, JaCoCo, release profile
├── README.md ← Plugin README with step examples
├── LICENSE ← Apache 2.0
├── cliff.toml ← git-cliff changelog config
├── .gitignore
├── .spotless/
│ └── HEADER.txt ← Apache 2.0 license header (with your name)
└── src/
├── main/
│ ├── java/.../
│ │ ├── MyPluginPlugin.java ← KtestifyPlugin SPI implementation
│ │ ├── config/
│ │ │ └── MyPluginConfig.java ← Typed HOCON config reader
│ │ ├── io/
│ │ │ ├── MyPluginRecordFetcher.java ← RecordFetcher<String> (transport layer)
│ │ │ ├── MyPluginConsumer.java ← Orchestration (fetch → match → result)
│ │ │ └── MyPluginConsumerContext.java ← Immutable per-fetch context
│ │ ├── entities/
│ │ │ └── KtestifyMyPluginEntity.java ← Resource entity
│ │ ├── services/
│ │ │ ├── MyPluginActionService.java ← Send/upload service
│ │ │ └── MyPluginValidationService.java ← Validation + timeout orchestration
│ │ └── steps/
│ │ ├── MyPluginBackgroundSteps.java ← @Given steps
│ │ ├── MyPluginActionSteps.java ← @When steps
│ │ ├── MyPluginValidationSteps.java ← @Then / @And steps
│ │ └── SharedMyPluginResources.java ← PicoContainer shared state
│ └── resources/
│ ├── reference.conf ← Default HOCON config
│ ├── log4j2.properties ← Logging config
│ └── META-INF/services/
│ └── io.github.ktestify.plugin.KtestifyPlugin
└── test/
└── java/.../
├── MyPluginPluginTest.java ← Plugin lifecycle tests
└── config/
└── MyPluginConfigTest.java ← Config loading tests
Three-layer architecture
The generated project follows the same three-layer separation as ktestify-core:
┌──────────────────────────────────────────────────────────────┐
│ TRANSPORT │ ORCHESTRATION │ ASSERTION │
│ MyPluginRecordFetcher │ MyPluginConsumer│ RecordMatcher │
│ implements RecordFetcher │ │ (from core) │
└──────────────────────────────────────────────────────────────┘
- Transport:
MyPluginRecordFetcherimplementsRecordFetcher<String>from ktestify-core. It returnsList<ConsumedRecord<String>>, the only type crossing layer boundaries. - Orchestration:
MyPluginConsumerwires fetch to match to result. It usesRecordMatcherFactory.forRaw()to select the correct matcher. - Assertion: all standard
RecordMatcherimplementations from ktestify-core are reused as-is. No matcher code is needed in your plugin.
Phase 1: Generate the Project
Step 1.1. Run the archetype
mvn archetype:generate \
-DarchetypeGroupId=io.github.ktestify \
-DarchetypeArtifactId=ktestify-plugin-archetype \
-DarchetypeVersion=1.0-SNAPSHOT \
-DgroupId=io.github.ktestify \
-DartifactId=ktestify-plugin-s3 \
-Dversion=1.0-SNAPSHOT \
-Dpackage=io.github.ktestify.s3 \
-DpluginShortName=s3 \
-DpluginKebabId=s3 \
-DpluginPascalName=S3 \
-DauthorName="Your Name" \
-DauthorEmail="your.email@example.com" \
-DgithubOrg=your-github-username \
-DgithubRepoName=ktestify-plugin-s3 \
-DinteractiveMode=false
Step 1.2. Archetype parameters
| Parameter | Description | Example |
|---|---|---|
groupId | Maven groupId for the generated project | io.github.ktestify |
artifactId | Maven artifactId (should start with ktestify-plugin-) | ktestify-plugin-s3 |
version | Initial Maven version | 1.0-SNAPSHOT |
package | Base Java package | io.github.ktestify.s3 |
pluginShortName | Short lowercase name (file names, log files) | s3 |
pluginKebabId | Kebab-case plugin ID (HOCON config paths) | s3 |
pluginPascalName | PascalCase name (Java class names) | S3 |
authorName | Author display name (startup banner + POM) | Your Name |
authorEmail | Author contact email | your.email@example.com |
githubOrg | GitHub username or organisation | your-github-username |
githubRepoName | GitHub repository name | ktestify-plugin-s3 |
ktestify-core version: The generated project depends on the latest released version of
ktestify-core. This version is managed by Dependabot in the archetype repository. When a new version of ktestify-core is released, Dependabot automatically opens a PR to bump it in the template POM.
Step 1.3. Verify it compiles
cd ktestify-plugin-s3
mvn compile
If this succeeds, you have a valid plugin skeleton. The generated code compiles out of the box, and all TODO stubs are syntactically valid.
Phase 2: Implement Your Transport
The generated code contains TODO markers where you need to add your transport-specific logic. Here's what to implement:
Step 2.1. Add your transport SDK dependency
Add your transport SDK to pom.xml:
<!-- Example: AWS S3 SDK -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.0</version>
</dependency>
Step 2.2. Implement the RecordFetcher
Open src/main/java/.../io/S3RecordFetcher.java and implement the fetch() method:
@Override
public List<ConsumedRecord<String>> fetch() throws FetchException {
String recordId = context.getRecordId();
String resourceName = context.getResourceName();
long deadlineMs = System.currentTimeMillis() + resolveReadTimeoutMs();
long pollMs = resolvePollIntervalMs();
while (System.currentTimeMillis() < deadlineMs) {
// TODO: Replace with your SDK call
boolean exists = s3Client.headObject(
HeadObjectRequest.builder()
.bucket(resourceName)
.key(recordId)
.build()
).sdkHttpResponse().isSuccessful();
if (exists) {
return List.of(downloadRecord(resourceName, recordId));
}
sleep(pollMs, recordId);
}
throw new FetchException("Timed out waiting for record '" + recordId + "'");
}
Step 2.3. Implement the Action Service
Open src/main/java/.../services/S3ActionService.java and implement the send() method:
public void send(KtestifyS3Entity resource, String recordId, String sourceFile) {
String connStr = resolveConnectionString(resource);
// TODO: Build your client and upload the file
byte[] content = Files.readAllBytes(Path.of(sourceFile));
s3Client.putObject(
PutObjectRequest.builder()
.bucket(resource.getResourceName())
.key(recordId)
.build(),
RequestBody.fromBytes(content)
);
}
Step 2.4. Customize step wording (optional)
The generated steps use generic wording like "S3 resource" and "expected S3 record from file". You can rename these to be more natural for your transport:
// Before (generated)
@Given("S3 resource")
// After (customized)
@Given("S3 bucket")
Phase 3: Configuration
Step 3.1. Update reference.conf
The generated reference.conf has a basic connection-string pattern. Update it to match your transport's authentication model:
ktestify.plugins.s3 {
# AWS credentials (uses DefaultCredentialsProvider if not set)
access-key-id = ""
access-key-id = ${?AWS_ACCESS_KEY_ID}
secret-access-key = ""
secret-access-key = ${?AWS_SECRET_ACCESS_KEY}
region = "us-east-1"
region = ${?AWS_REGION}
endpoint = "" # for LocalStack
endpoint = ${?AWS_ENDPOINT}
read-timeout = 30s
poll-interval = 500ms
}
Step 3.2. Update the Config class
Add fields to S3Config.java to match your new config keys:
private S3Config(Config cfg) {
this.accessKeyId = cfg.getString("access-key-id");
this.secretAccessKey = cfg.getString("secret-access-key");
this.region = cfg.getString("region");
this.endpoint = cfg.getString("endpoint");
this.readTimeoutMs = cfg.getDuration("read-timeout").toMillis();
this.pollIntervalMs = cfg.getDuration("poll-interval").toMillis();
}
Phase 4: Testing
Step 4.1. Unit tests
The archetype generates unit tests for the plugin lifecycle and config loading. Run them:
mvn test
Step 4.2. Integration tests
Add integration tests using Testcontainers. Create src/test/java/.../S3PluginIT.java:
@Testcontainers
class S3PluginIT {
@Container
static final GenericContainer<?> localstack =
new GenericContainer<>("localstack/localstack:3.0")
.withEnv("SERVICES", "s3")
.withExposedPorts(4566);
@Test
@DisplayName("upload and validate a blob")
void uploadAndValidate() {
// Point your plugin at LocalStack
// Upload a file via the action service
// Assert it appears via the validation service
}
}
Run integration tests (requires Docker):
mvn verify
Phase 5: Use Your Plugin
Option A. As a Maven dependency
<dependency>
<groupId>io.github.ktestify</groupId>
<artifactId>ktestify-plugin-s3</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
Option B. Drop the JAR into /workspace/plugins
docker run --rm \
-v $(pwd)/features:/workspace/features \
-v $(pwd)/plugins:/workspace/plugins \
ghcr.io/ktestify/ktestify-cucumber:latest \
/workspace/features
Example Gherkin scenario
Feature: S3 integration
Background:
Given S3 bucket
| resourceName | resourceAlias |
| test-bucket | blobs |
Scenario: Upload and validate
When S3 record is sent from file
| resourceAlias | file | recordId |
| blobs | payload.json | data.json |
Then expected S3 record from file
| resourceAlias | recordId | file | readTimeout |
| blobs | data.json | expected.json | 30 |
Checklist for Production Quality
Before publishing your plugin, ensure it meets these criteria:
- Plugin implements all methods of
KtestifyPlugininterface - Configuration is loaded from
ktestify.plugins.<id>HOCON subtree -
initialize()validates required config and throwsPluginExceptionon failure -
shutdown()releases all resources (connections, threads, etc.) - Plugin is registered in
META-INF/services/io.github.ktestify.plugin.KtestifyPlugin - Step definitions are in a discoverable package (returned by
getGluePackage()) -
RecordFetcher<V>implementation returnsConsumedRecord<V>correctly -
reference.confdocuments all configuration keys with sensible defaults - Logging uses SLF4J via
@Slf4jannotation - Documentation includes configuration examples and usage scenarios
- README contains a clear description of what the plugin does
- Unit and integration tests pass
- Code is formatted with Maven Spotless (
mvn spotless:apply)
Reference Implementations
Looking for real-world examples? Check out the first-party plugins:
| Plugin | What it demonstrates |
|---|---|
| ktestify-plugin-azureblob | Azure Blob Storage transport with Azurite integration tests |
| ktestify-plugin-notifications | Non-transport plugin: Teams/Slack/webhook notifications with its own SPI |
| ktestify-plugin-skeleton | The reference template the archetype is derived from |
Resources
- ktestify-plugin-archetype: the archetype repository
- Plugin System Documentation: how the plugin loading works
- ktestify-core API: base classes and interfaces
- Adding a Transport: transport layer deep dive
Next Steps
Once your plugin is complete and tested:
- Publish to Maven Central: follow Sonatype's guide
- Create a GitHub repository: use
ktestify-plugin-*naming convention - Document on docs.ktestify.xyz: add a page next to the Azure Blob example
- Announce in the ktestify community: open a discussion thread
Happy plugin building!