Skip to main content
Version: 5.1.0 (Unmaintained)

Best Practices

Action factory method pattern​

Create each action using a private static method. This makes the test structure visible in the IDE outline and enables direct click-to-navigate:

@Paramixel.Factory
public static Action<?> factory() {
Action<?> first = first();
Action<?> second = second();

return Lifecycle.of("MyTest")
.child(first)
.child(second)
.resolve();
}

private static Action<?> first() {
return Step.of("first", ctx -> { /* test logic */ });
}

private static Action<?> second() {
return Step.of("second", ctx -> { /* test logic */ });
}

Always assign actions to local variables before passing to specs. Avoid inline method calls inside spec chains.

Choose the right action type​

Use caseAction
Single stepStep
Setup/body/teardown with managed instanceInstance
Setup/body/teardown without instanceLifecycle or Static
Ordered dependent testsLifecycle (dependent)
Ordered independent testsSequential (independent)
Concurrent testsParallel
Static setup/teardown (no instance)Static

Use Instance for managed state​

When tests need a shared mutable object with automatic cleanup, use Instance:

Instance.of("user-service", UserService::new)
.child("create-user", UserService::createUser)
.child("get-user", UserService::getUser)
.resolve();

Instance auto-generates Instantiate and Destroy steps for setup and teardown. AutoCloseable instances are automatically closed in the generated Destroy step.

Use AnnotationResolver for annotated methods​

For test classes with many methods, AnnotationResolver resolves @Paramixel.Id methods into Step actions:

var resolver = AnnotationResolver.create(MyTest.class);

Instance.of(MyTest.class)
.child(resolver.byId("login"))
.child(resolver.byId("verify"))
.child(resolver.byId("logout"))
.resolve();

Use the Spec + loop pattern for parameterized tests​

Lifecycle, Sequential, Parallel, Instance, and Static also provide each(Iterable, Function) as a convenience method that produces the same tree:

For-loop​

var spec = Parallel.of("suite").parallelism(4);
for (TestCase tc : testCases()) {
spec.child(argument(tc));
}
return spec.resolve();

Each convenience method​

return Parallel.of("suite")
.parallelism(4)
.each(testCases(), tc -> argument(tc))
.resolve();

Keep actions immutable​

Actions are reusable across runs. Do not store mutable state in action fields. Use Instance for per-execution state, or Context#instance(Class).

Respect the Action contract​

When implementing custom actions:

  1. Set the descriptor status to RUNNING and then to a terminal Status.
  2. Fire listener callbacks before and after the action boundary using listener.onBeforeExecution(descriptor) and listener.onAfterExecution(descriptor).
  3. Respect Mode — handle SKIP and ABORT modes.
  4. For most use cases, prefer composing built-in actions rather than implementing Action directly.

Use CleanUp for resource teardown​

CleanUp captures teardown failures and can aggregate multiple cleanup failures with suppressed exceptions:

import org.paramixel.api.support.CleanUp;

CleanUp.runAndThrow(
CleanUp.of(() -> closeResourceA()),
CleanUp.of(resourceB));

Use Retry for transient failures​

Retry retries an operation with configurable backoff:

import org.paramixel.api.support.Retry;
import org.paramixel.api.support.Retry.Policy;

Retry.of(Policy.exponential(Duration.ofMillis(100), Duration.ofSeconds(5)))
.run(() -> {
// operation that may fail transiently
});

See Retry and CleanUp for the full API.

Configure reports for CI​

Always configure a report file in CI for archival:

./mvnw test -Dparamixel.report.file=target/paramixel/report.json
./gradlew paramixelTest -Dparamixel.report.file=build/paramixel/report.json

Set parallelism appropriately​

On CI runners with limited CPU, set an explicit parallelism to prevent oversubscription:

./mvnw test -Dparamixel.parallelism=4

Next steps​