Action Composition
Paramixel test plans are ordinary Action trees. In 4.x, ordered composition uses Container and concurrent composition uses Parallel.
Composition actions are builder-only. Noop.of(...) remains as the compact factory for placeholder actions.
public class MyTest {
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() {
Action setup = setup();
Action test = test();
Action cleanup = cleanup();
return Container.builder("suite")
.before(setup)
.child(test)
.after(cleanup)
.build();
}
private static Action setup() {
return Direct.builder("setup").runnable(context -> {}).build();
}
private static Action test() {
return Direct.builder("test").runnable(context -> {}).build();
}
private static Action cleanup() {
return Direct.builder("cleanup").runnable(context -> {}).build();
}
}
Use Container.Policy for run-all, fail-fast, declared, or shuffled body execution.
Common composition patterns
Ordered run-all
public class MyTest {
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() {
Action step1 = step1();
Action step2 = step2();
return Container.builder("suite")
.policy(Container.Policy.builder()
.childMode(Container.ChildMode.INDEPENDENT)
.build())
.child(step1)
.child(step2)
.build();
}
private static Action step1() {
return Direct.builder("step 1").runnable(context -> {}).build();
}
private static Action step2() {
return Direct.builder("step 2").runnable(context -> {}).build();
}
}
Ordered fail-fast
Container defaults to dependent, declared-order body execution.
public class MyTest {
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() {
Action step1 = step1();
Action step2 = step2();
return Container.builder("suite")
.child(step1)
.child(step2)
.build();
}
private static Action step1() {
return Direct.builder("step 1").runnable(context -> {}).build();
}
private static Action step2() {
return Direct.builder("step 2").runnable(context -> {}).build();
}
}
Shuffled run-all
public class MyTest {
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() {
Action a = a();
Action b = b();
return Container.builder("suite")
.policy(Container.Policy.builder()
.childMode(Container.ChildMode.INDEPENDENT)
.orderMode(Container.OrderMode.SHUFFLED)
.seed(1234L)
.build())
.child(aAction)
.child(bAction)
.build();
}
private static Action a() {
return Direct.builder("a").runnable(context -> {}).build();
}
private static Action b() {
return Direct.builder("b").runnable(context -> {}).build();
}
}
Concurrent children
public class MyTest {
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() {
Action a = a();
Action b = b();
return Parallel.builder("suite")
.parallelism(2)
.child(aAction)
.child(bAction)
.build();
}
private static Action a() {
return Direct.builder("a").runnable(context -> {}).build();
}
private static Action b() {
return Direct.builder("b").runnable(context -> {}).build();
}
}
Setup / body / teardown
public class MyTest {
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() {
Action before = before();
Action primary = primary();
Action after = after();
return Container.builder("suite")
.before(before)
.child(primary)
.after(after)
.build();
}
private static Action before() {
return Direct.builder("before")
.runnable(context -> context.getStore().put("token", "abc"))
.build();
}
private static Action primary() {
return Direct.builder("primary").runnable(context -> {}).build();
}
private static Action after() {
return Direct.builder("after").runnable(context -> {}).build();
}
}
Nesting
Actions can be nested arbitrarily.
public class MyTest {
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() {
Action case1 = case1();
Action finished = Noop.of("finished");
return Container.builder("suite")
.child(case1)
.child(finished)
.build();
}
private static Action case1() {
Action before = before();
Action checks = checks();
Action after = after();
return Container.builder("case 1")
.before(before)
.child(checks)
.after(after)
.build();
}
private static Action before() {
return Direct.builder("before").runnable(context -> {}).build();
}
private static Action checks() {
Action a = a();
Action b = b();
return Parallel.builder("checks")
.child(aAction)
.child(bAction)
.build();
}
private static Action a() {
return Direct.builder("a").runnable(context -> {}).build();
}
private static Action b() {
return Direct.builder("b").runnable(context -> {}).build();
}
private static Action after() {
return Direct.builder("after").runnable(context -> {}).build();
}
}
Choosing a Container Policy
ChildMode.INDEPENDENTruns every body child.ChildMode.DEPENDENTstops body execution after the first failed or skipped child.OrderMode.DECLAREDruns body children in builder order.OrderMode.SHUFFLEDshuffles body children, optionally withseed(...).
Argument testing pattern
When running the same test logic against multiple inputs, use a builder with a loop to add children dynamically:
var builder = Container.builder("suite")
.policy(Container.Policy.builder()
.childMode(Container.ChildMode.INDEPENDENT)
.build());
for (String argument : arguments()) {
builder.child(argument(argument));
}
return builder.build();
For concurrent argument execution, use Parallel instead:
var builder = Parallel.builder("suite").parallelism(4);
for (String argument : arguments()) {
builder.child(argument(argument));
}
return builder.build();
See Argument Testing for the full pattern with per-argument lifecycle, context sharing, and type examples.
Action factory method pattern
For readability and IDE navigability, create each action using a private static method. This pattern makes the test structure visible in the IDE outline and allows direct click-to-navigate for each action.
Basic pattern
public class MyTest {
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() {
Action first = first();
Action second = second();
return Container.builder("MyTest")
.child(first)
.child(second)
.build();
}
private static Action first() {
return Direct.builder("first")
.runnable(context -> { /* test logic */ })
.build();
}
private static Action second() {
return Direct.builder("second")
.runnable(context -> { /* test logic */ })
.build();
}
}
Integration test pattern
For integration tests with setup/teardown, extract each lifecycle phase into its own method:
public class KafkaTest {
private static final String ENVIRONMENT = "environment";
public static void main(String[] args) {
Factory.defaultRunner().runAndExit(actionFactory());
}
@Paramixel.ActionFactory
public static Action actionFactory() throws Throwable {
var parallelBuilder = Parallel.builder("KafkaExample");
for (KafkaTestEnvironment environment : KafkaTestEnvironment.createTestEnvironments()) {
Action argumentContainer = argument(environment);
parallelBuilder.child(argumentContainer);
}
return parallelBuilder.build();
}
private static Action argument(KafkaTestEnvironment environment) {
Action before = before(environment);
Action test = test();
Action after = after(environment);
return Container.builder(environment.name())
.before(before)
.child(test)
.after(after)
.build();
}
private static Action before(KafkaTestEnvironment environment) {
return Direct.builder("before")
.runnable(context -> {
environment.initialize();
context.getStore().put(ENVIRONMENT, environment);
})
.build();
}
private static Action test() {
return Direct.builder("test")
.runnable(context -> {
KafkaTestEnvironment env = context.getAncestor("../")
.getStore()
.get(ENVIRONMENT, KafkaTestEnvironment.class)
.orElseThrow();
// test logic using env
})
.build();
}
private static Action after(KafkaTestEnvironment environment) {
return Direct.builder("after")
.runnable(context -> {
KafkaTestEnvironment env = context.getStore()
.remove(ENVIRONMENT, KafkaTestEnvironment.class)
.orElseThrow();
env.close();
})
.build();
}
}
Benefits
-
IDE navigation — Each
private static Actionmethod appears in the IDE outline. Click to navigate directly to any action's implementation. -
Readable structure — The
actionFactory()method reads as a high-level outline of the test structure. -
Builder + loop pattern — When adding multiple children, create the builder first, then loop to add children:
var builder = Container.builder("suite")
.policy(Container.Policy.builder()
.childMode(Container.ChildMode.INDEPENDENT)
.build());
for (String name : names) {
Action action = createTest(name);
builder.child(action);
}
return builder.build();
- Local variables — Always assign actions to local variables before passing to builders. This makes the code scannable and avoids inline method calls inside builder chains.