This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new d971d1ff48de CAMEL-23147: Embed citrus test plugin in camel-launcher
(#21979)
d971d1ff48de is described below
commit d971d1ff48dec1b47eb4fb624a94ef1fdbc9d81b
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Mar 17 08:12:23 2026 +0100
CAMEL-23147: Embed citrus test plugin in camel-launcher (#21979)
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
dsl/camel-jbang/camel-jbang-plugin-test/pom.xml | 2 +-
.../dsl/jbang/core/commands/test/TestInit.java | 158 ++++++++++++++
.../dsl/jbang/core/commands/test/TestPlugin.java | 148 +------------
.../dsl/jbang/core/commands/test/TestRun.java | 229 +++++++++++++++++++++
.../main/resources/templates/citrus-feature.tmpl | 8 +
.../main/resources/templates/citrus-groovy.tmpl | 5 +
.../src/main/resources/templates/citrus-java.tmpl | 21 ++
.../src/main/resources/templates/citrus-xml.tmpl | 12 ++
.../src/main/resources/templates/citrus-yaml.tmpl | 10 +
dsl/camel-jbang/camel-launcher/pom.xml | 6 +-
.../dsl/jbang/launcher/CamelLauncherMain.java | 2 +
11 files changed, 456 insertions(+), 145 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-plugin-test/pom.xml
b/dsl/camel-jbang/camel-jbang-plugin-test/pom.xml
index b660622ff8ea..593ee0288eee 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-test/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-test/pom.xml
@@ -49,7 +49,7 @@
<dependency>
<groupId>org.citrusframework</groupId>
- <artifactId>citrus-jbang-connector</artifactId>
+ <artifactId>citrus-base</artifactId>
<version>${citrus-version}</version>
</dependency>
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestInit.java
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestInit.java
new file mode 100644
index 000000000000..7bc2aedbabdd
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestInit.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.ExportHelper;
+import org.apache.camel.util.IOHelper;
+import org.citrusframework.CitrusVersion;
+import picocli.CommandLine;
+
+/**
+ * Initializes a new Citrus test file from a template. Creates the test
subfolder if it does not exist and writes a
+ * template test source for the given file type. Also creates a
jbang.properties file with default Citrus dependencies
+ * when not already present.
+ */
[email protected](name = "init",
+ description = "Initialize a new Citrus test from a
template")
+public class TestInit extends CamelCommand {
+
+ public static final String TEST_DIR = "test";
+
+ @CommandLine.Parameters(index = "0", description = "Test file name (e.g.
MyTest.yaml, MyTest.xml, MyTest.java)")
+ String file;
+
+ @CommandLine.Option(names = { "-d", "--directory" }, description = "Target
directory", defaultValue = ".")
+ String directory;
+
+ public TestInit(CamelJBangMain main) {
+ super(main);
+ }
+
+ @Override
+ public Integer doCall() throws Exception {
+ // Determine file extension and base name
+ String ext = getFileExtension(file);
+ String baseName = getBaseName(file);
+
+ if (ext.isEmpty()) {
+ printer().println("Cannot determine file type for: " + file);
+ return 1;
+ }
+
+ // Load template for the file type
+ String template;
+ try (InputStream is =
TestInit.class.getClassLoader().getResourceAsStream("templates/citrus-" + ext +
".tmpl")) {
+ if (is == null) {
+ printer().println("Unsupported test file type: " + ext);
+ return 1;
+ }
+ template = IOHelper.loadText(is);
+ }
+
+ // Replace template placeholders
+ template = template.replaceAll("\\{\\{ \\.Name }}", baseName);
+
+ // Determine the working directory
+ Path workingDir = resolveTestDir();
+ if (workingDir == null) {
+ printer().println("Cannot create test working directory");
+ return 1;
+ }
+
+ // Create target directory if specified
+ Path targetDir;
+ if (".".equals(directory)) {
+ targetDir = workingDir;
+ } else {
+ targetDir = workingDir.resolve(directory);
+ Files.createDirectories(targetDir);
+ }
+
+ // Create jbang properties with default dependencies if not present
+ createJBangProperties(workingDir);
+
+ // Write the test file
+ Path testFile = targetDir.resolve(file);
+ Files.writeString(testFile, template);
+
+ printer().println("Created test file: " + testFile);
+ return 0;
+ }
+
+ /**
+ * Resolves and creates the test directory. Automatically uses the test
subfolder as the working directory.
+ */
+ private Path resolveTestDir() {
+ Path currentDir = Paths.get(".");
+ Path workingDir;
+ if
(TEST_DIR.equals(currentDir.toAbsolutePath().normalize().getFileName().toString()))
{
+ // current directory is already the test subfolder
+ workingDir = currentDir;
+ } else if (currentDir.resolve(TEST_DIR).toFile().exists()) {
+ // navigate to existing test subfolder
+ workingDir = currentDir.resolve(TEST_DIR);
+ } else if (currentDir.resolve(TEST_DIR).toFile().mkdirs()) {
+ // create test subfolder and navigate to it
+ workingDir = currentDir.resolve(TEST_DIR);
+ } else {
+ return null;
+ }
+ return workingDir;
+ }
+
+ /**
+ * Creates jbang.properties with default Citrus dependencies if not
already present.
+ */
+ private void createJBangProperties(Path workingDir) {
+ if (!workingDir.resolve("jbang.properties").toFile().exists()) {
+ Path jbangProperties = workingDir.resolve("jbang.properties");
+ try (InputStream is
+ =
TestInit.class.getClassLoader().getResourceAsStream("templates/jbang-properties.tmpl"))
{
+ String context = IOHelper.loadText(is);
+ context = context.replaceAll("\\{\\{ \\.CitrusVersion }}",
CitrusVersion.version());
+ ExportHelper.safeCopy(new
ByteArrayInputStream(context.getBytes(StandardCharsets.UTF_8)),
jbangProperties);
+ } catch (Exception e) {
+ printer().println("Failed to create jbang.properties for tests
in: " + jbangProperties);
+ }
+ }
+ }
+
+ private static String getFileExtension(String fileName) {
+ int dotIndex = fileName.lastIndexOf('.');
+ if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
+ return fileName.substring(dotIndex + 1);
+ }
+ return "";
+ }
+
+ private static String getBaseName(String fileName) {
+ int dotIndex = fileName.lastIndexOf('.');
+ if (dotIndex > 0) {
+ return fileName.substring(0, dotIndex);
+ }
+ return fileName;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestPlugin.java
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestPlugin.java
index a556df43b7c3..085fd9cb294d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestPlugin.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestPlugin.java
@@ -16,27 +16,12 @@
*/
package org.apache.camel.dsl.jbang.core.commands.test;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
import java.util.Optional;
-import org.apache.camel.RuntimeCamelException;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
-import org.apache.camel.dsl.jbang.core.commands.ExportHelper;
import org.apache.camel.dsl.jbang.core.common.CamelJBangPlugin;
import org.apache.camel.dsl.jbang.core.common.Plugin;
import org.apache.camel.dsl.jbang.core.common.PluginExporter;
-import org.apache.camel.util.IOHelper;
-import org.citrusframework.CitrusVersion;
-import org.citrusframework.jbang.JBangSettings;
-import org.citrusframework.jbang.JBangSupport;
-import org.citrusframework.jbang.ProcessAndOutput;
import picocli.CommandLine;
@CamelJBangPlugin(name = "camel-jbang-plugin-test", firstVersion = "4.14.0")
@@ -44,138 +29,15 @@ public class TestPlugin implements Plugin {
@Override
public void customize(CommandLine commandLine, CamelJBangMain main) {
- commandLine.setExecutionStrategy(new CitrusExecutionStrategy(main))
- .addSubcommand("test", new CommandLine(new TestCommand(main))
- .setUnmatchedArgumentsAllowed(true)
- .setUnmatchedOptionsAllowedAsOptionParameters(true));
+ var cmd = new CommandLine(new TestCommand(main))
+ .addSubcommand("init", new CommandLine(new TestInit(main)))
+ .addSubcommand("run", new CommandLine(new TestRun(main)));
+
+ commandLine.addSubcommand("test", cmd);
}
@Override
public Optional<PluginExporter> getExporter() {
return Optional.of(new TestPluginExporter());
}
-
- /**
- * Command execution strategy delegates to Citrus JBang for subcommands
like init or run. Performs special command
- * preparations and makes sure to run the proper Citrus version for this
Camel release.
- *
- * @param main Camel JBang main that provides the output printer.
- */
- private record CitrusExecutionStrategy(CamelJBangMain main) implements
CommandLine.IExecutionStrategy {
-
- public static final String TEST_DIR = "test";
-
- @Override
- public int execute(CommandLine.ParseResult parseResult)
- throws CommandLine.ExecutionException,
CommandLine.ParameterException {
-
- String command;
- List<String> args = Collections.emptyList();
-
- if (parseResult.originalArgs().size() > 2) {
- command = parseResult.originalArgs().get(1);
- args = parseResult.originalArgs().subList(2,
parseResult.originalArgs().size());
- } else if (parseResult.originalArgs().size() == 2) {
- command = parseResult.originalArgs().get(1);
- } else {
- // run help command by default
- command = "--help";
- }
-
- JBangSupport citrus =
JBangSupport.jbang().app(JBangSettings.getApp())
- .withSystemProperty("citrus.jbang.version",
CitrusVersion.version());
-
- // Prepare commands
- if ("init".equals(command)) {
- return executeInitCommand(citrus, args);
- } else if ("run".equals(command)) {
- return executeRunCommand(citrus, args);
- }
-
- return execute(citrus, command, args);
- }
-
- /**
- * Prepare and execute init command. Automatically uses test subfolder
as a working directory for creating new
- * tests. Automatically adds a jbang.properties configuration to add
required Camel Citrus dependencies.
- */
- private int executeInitCommand(JBangSupport citrus, List<String> args)
{
- Path currentDir = Paths.get(".");
- Path workingDir;
- // Automatically set test subfolder as a working directory
- if (TEST_DIR.equals(currentDir.getFileName().toString())) {
- // current directory is already the test subfolder
- workingDir = currentDir;
- } else if (currentDir.resolve(TEST_DIR).toFile().exists()) {
- // navigate to existing test subfolder
- workingDir = currentDir.resolve(TEST_DIR);
- citrus.workingDir(workingDir);
- } else if (currentDir.resolve(TEST_DIR).toFile().mkdirs()) {
- // create test subfolder and navigate to it
- workingDir = currentDir.resolve(TEST_DIR);
- citrus.workingDir(workingDir);
- } else {
- throw new RuntimeCamelException("Cannot create test working
directory in: " + currentDir);
- }
-
- // Create jbang properties with default dependencies if not present
- if (!workingDir.resolve("jbang.properties").toFile().exists()) {
- Path jbangProperties = workingDir.resolve("jbang.properties");
- try (InputStream is
- =
TestPlugin.class.getClassLoader().getResourceAsStream("templates/jbang-properties.tmpl"))
{
- String context = IOHelper.loadText(is);
-
- context = context.replaceAll("\\{\\{ \\.CitrusVersion }}",
CitrusVersion.version());
-
- ExportHelper.safeCopy(new
ByteArrayInputStream(context.getBytes(StandardCharsets.UTF_8)),
jbangProperties);
- } catch (Exception e) {
- main.getOut().println("Failed to create jbang.properties
for tests in:" + jbangProperties);
- }
- }
-
- return execute(citrus, "init", args);
- }
-
- /**
- * Prepare and execute Citrus run command. Automatically navigates to
test subfolder if it is present and uses
- * this as a working directory. Runs command asynchronous streaming
logs to the main output of this Camel JBang
- * process.
- */
- private int executeRunCommand(JBangSupport citrus, List<String> args) {
- Path currentDir = Paths.get(".");
- List<String> runArgs = new ArrayList<>(args);
- // automatically navigate to test subfolder for test execution
- if (currentDir.resolve(TEST_DIR).toFile().exists()) {
- // set test subfolder as working directory
- citrus.workingDir(currentDir.resolve(TEST_DIR));
-
- // remove test folder prefix in test file path if present
- if (!args.isEmpty() && args.get(0).startsWith(TEST_DIR + "/"))
{
- runArgs = new ArrayList<>(args.subList(1, args.size()));
- runArgs.add(0, args.get(0).substring((TEST_DIR +
"/").length()));
- }
- }
-
- citrus.withOutputListener(output -> main.getOut().print(output));
- ProcessAndOutput pao = citrus.runAsync("run", runArgs);
- try {
- pao.waitFor();
- } catch (InterruptedException e) {
- main.getOut().printErr("Interrupted while running Citrus
command", e);
- }
-
- return pao.getProcess().exitValue();
- }
-
- /**
- * Uses given Citrus JBang instance to run the given command using the
given arguments.
- *
- * @return exit code of the command process.
- */
- private int execute(JBangSupport citrus, String command, List<String>
args) {
- ProcessAndOutput pao = citrus.run(command, args);
- main.getOut().print(pao.getOutput());
- return pao.getProcess().exitValue();
- }
- }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestRun.java
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestRun.java
new file mode 100644
index 000000000000..6d1094ada282
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/java/org/apache/camel/dsl/jbang/core/commands/test/TestRun.java
@@ -0,0 +1,229 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.test;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.citrusframework.TestSource;
+import org.citrusframework.main.TestEngine;
+import org.citrusframework.main.TestRunConfiguration;
+import org.citrusframework.util.FileUtils;
+import picocli.CommandLine;
+
+/**
+ * Runs Citrus test files directly using the Citrus test engine. This replaces
the indirect JBang process call with a
+ * direct in-process test execution.
+ */
[email protected](name = "run",
+ description = "Run Citrus tests")
+public class TestRun extends CamelCommand {
+
+ public static final String TEST_DIR = "test";
+ public static final String WORK_DIR = ".citrus-jbang";
+
+ private static final String[] ACCEPTED_FILE_EXT = { "xml", "yaml", "yml",
"java", "groovy", "feature" };
+
+ @CommandLine.Parameters(description = "Test files or directories to run",
arity = "0..*")
+ String[] files;
+
+ @CommandLine.Option(names = { "--engine" }, description = "Test engine to
use", defaultValue = "default")
+ String engine;
+
+ @CommandLine.Option(names = { "--verbose" }, description = "Enable verbose
output")
+ boolean verbose;
+
+ @CommandLine.Option(names = { "-p", "--property" }, description = "Set
system properties (key=value)")
+ String[] properties;
+
+ @CommandLine.Option(names = { "--includes" }, description = "Test name
include patterns")
+ String[] includes;
+
+ public TestRun(CamelJBangMain main) {
+ super(main);
+ }
+
+ @Override
+ public Integer doCall() throws Exception {
+ Path currentDir = Paths.get(".");
+
+ // Determine working directory (prefer test subfolder)
+ Path workDir;
+ if (currentDir.resolve(TEST_DIR).toFile().exists()) {
+ workDir = currentDir.resolve(TEST_DIR);
+ } else {
+ workDir = currentDir;
+ }
+
+ // Set up system properties
+ Map<String, String> props = new HashMap<>();
+ if (properties != null) {
+ for (String prop : properties) {
+ String[] parts = prop.split("=", 2);
+ if (parts.length == 2) {
+ props.put(parts[0], parts[1]);
+ }
+ }
+ }
+
+ // Apply system properties
+ for (Map.Entry<String, String> entry : props.entrySet()) {
+ System.setProperty(entry.getKey(), entry.getValue());
+ }
+
+ // Clean and create work directory
+ File citrusWorkDir = new File(WORK_DIR);
+ removeDir(citrusWorkDir);
+ if (!citrusWorkDir.mkdirs()) {
+ printer().println("Failed to create working directory " +
WORK_DIR);
+ return 1;
+ }
+
+ // Resolve test sources
+ List<String> testSources = new ArrayList<>();
+ resolveTests(files, testSources, workDir);
+
+ // If no explicit files given, scan current directory
+ if (testSources.isEmpty()) {
+ resolveTests(new String[] { workDir.toString() }, testSources,
workDir);
+ }
+
+ if (testSources.isEmpty()) {
+ printer().println("No test files found");
+ return 1;
+ }
+
+ // Build test run configuration
+ TestRunConfiguration configuration = getRunConfiguration(testSources,
workDir);
+
+ // Look up and run the test engine
+ TestEngine testEngine = TestEngine.lookup(configuration);
+ testEngine.run();
+
+ return 0;
+ }
+
+ /**
+ * Creates a test run configuration from the resolved test sources.
+ */
+ protected TestRunConfiguration getRunConfiguration(List<String>
testSources, Path workDir) {
+ String ext = FileUtils.getFileExtension(testSources.get(0));
+
+ TestRunConfiguration configuration = new TestRunConfiguration();
+ configuration.setWorkDir(workDir.toAbsolutePath().toString());
+
+ if (!"default".equals(engine)) {
+ configuration.setEngine(engine);
+ } else if ("feature".equals(ext)) {
+ configuration.setEngine("cucumber");
+ }
+
+ configuration.setVerbose(verbose);
+
+ if (includes != null) {
+ configuration.setIncludes(includes);
+ }
+
+ // Add test sources
+ List<TestSource> sources = new ArrayList<>();
+ for (String source : testSources) {
+ String sourceExt = FileUtils.getFileExtension(source);
+ String baseName = FileUtils.getBaseName(new
File(source).getName());
+ sources.add(new TestSource(sourceExt, baseName, source));
+ }
+ configuration.setTestSources(sources);
+
+ return configuration;
+ }
+
+ /**
+ * Resolves test file paths from the given arguments. Handles both
individual files and directories.
+ */
+ private void resolveTests(String[] testArgs, List<String> resolved, Path
workDir) {
+ if (testArgs == null) {
+ return;
+ }
+
+ for (String arg : testArgs) {
+ // Adjust path if it starts with test/ prefix and we're using the
test subfolder
+ String filePath = arg;
+ if (filePath.startsWith(TEST_DIR + "/")) {
+ filePath = filePath.substring((TEST_DIR + "/").length());
+ }
+
+ File resolved0 = workDir.resolve(filePath).toFile();
+ if (!resolved0.exists()) {
+ resolved0 = new File(filePath);
+ }
+ final File testFile = resolved0;
+
+ if (testFile.isDirectory()) {
+ // Scan directory for test files
+ String[] dirFiles = testFile.list();
+ if (dirFiles != null) {
+ String[] fullPaths = Arrays.stream(dirFiles)
+ .filter(f -> !skipFile(f))
+ .map(f -> new File(testFile, f).getPath())
+ .toArray(String[]::new);
+ resolveTests(fullPaths, resolved, workDir);
+ }
+ } else if (testFile.exists() && !skipFile(testFile.getName())) {
+ resolved.add(testFile.getPath());
+ }
+ }
+ }
+
+ /**
+ * Checks if a file should be skipped based on its extension.
+ */
+ private boolean skipFile(String fileName) {
+ if (fileName.startsWith(".")) {
+ return true;
+ }
+ String ext = FileUtils.getFileExtension(fileName);
+ return Arrays.stream(ACCEPTED_FILE_EXT).noneMatch(e -> e.equals(ext));
+ }
+
+ /**
+ * Recursively removes a directory.
+ */
+ private static void removeDir(File dir) {
+ if (dir.exists()) {
+ delete(dir);
+ }
+ }
+
+ private static void delete(File file) {
+ if (file.isDirectory()) {
+ File[] children = file.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ delete(child);
+ }
+ }
+ }
+ file.delete();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-feature.tmpl
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-feature.tmpl
new file mode 100644
index 000000000000..88c4e347d8a1
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-feature.tmpl
@@ -0,0 +1,8 @@
+Feature: {{ .Name }}
+
+ Background:
+ Given variables
+ | message | "Citrus rocks!" |
+
+ Scenario: Print message
+ Then print '${message}'
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-groovy.tmpl
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-groovy.tmpl
new file mode 100644
index 000000000000..0acce60ab4cc
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-groovy.tmpl
@@ -0,0 +1,5 @@
+variables {
+ message = "Citrus rocks!"
+}
+
+$(echo('${message}'))
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-java.tmpl
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-java.tmpl
new file mode 100644
index 000000000000..f99d9dbac01e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-java.tmpl
@@ -0,0 +1,21 @@
+import org.citrusframework.TestActionSupport;
+import org.citrusframework.TestCaseRunner;
+import org.citrusframework.annotations.CitrusResource;
+
+
+public class {{ .Name }} implements Runnable, TestActionSupport {
+
+ @CitrusResource
+ TestCaseRunner t;
+
+ @Override
+ public void run() {
+ t.given(
+ createVariables().variable("message", "Citrus rocks!")
+ );
+
+ t.then(
+ echo().message("${message}")
+ );
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-xml.tmpl
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-xml.tmpl
new file mode 100644
index 000000000000..e394315b7f6b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-xml.tmpl
@@ -0,0 +1,12 @@
+<test name="{{ .Name }}" author="Citrus" status="FINAL"
xmlns="http://citrusframework.org/schema/xml/testcase"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase
http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
+ <description>Sample test in XML</description>
+ <variables>
+ <variable name="message" value="Citrus rocks!"/>
+ </variables>
+
+ <actions>
+ <echo message="${message}"/>
+ </actions>
+</test>
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-yaml.tmpl
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-yaml.tmpl
new file mode 100644
index 000000000000..c88c44624115
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-test/src/main/resources/templates/citrus-yaml.tmpl
@@ -0,0 +1,10 @@
+name: {{ .Name }}
+author: Citrus
+status: FINAL
+description: Sample test in YAML
+variables:
+ - name: message
+ value: Citrus rocks!
+actions:
+ - echo:
+ message: "${message}"
diff --git a/dsl/camel-jbang/camel-launcher/pom.xml
b/dsl/camel-jbang/camel-launcher/pom.xml
index 3d876889e110..5f5a2a0bdf95 100644
--- a/dsl/camel-jbang/camel-launcher/pom.xml
+++ b/dsl/camel-jbang/camel-launcher/pom.xml
@@ -66,12 +66,16 @@
<!-- Pre-installed plugins -->
<!-- the edit plugin is not pre-installed -->
<!-- the route-parser plugin is not pre-installed -->
- <!-- the citrus test plugin cannot be embedded -->
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jbang-plugin-generate</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-jbang-plugin-test</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jbang-plugin-kubernetes</artifactId>
diff --git
a/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
b/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
index 594c916bfb81..8acfc37760ae 100644
---
a/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
+++
b/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.launcher;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.generate.GeneratePlugin;
import org.apache.camel.dsl.jbang.core.commands.kubernetes.KubernetesPlugin;
+import org.apache.camel.dsl.jbang.core.commands.test.TestPlugin;
import org.apache.camel.dsl.jbang.core.commands.validate.ValidatePlugin;
import picocli.CommandLine;
@@ -32,6 +33,7 @@ public class CamelLauncherMain extends CamelJBangMain {
// install embedded plugins
new GeneratePlugin().customize(commandLine, this);
new KubernetesPlugin().customize(commandLine, this);
+ new TestPlugin().customize(commandLine, this);
new ValidatePlugin().customize(commandLine, this);
}