Skip to content
Christoph Deppisch edited this page Sep 11, 2023 · 31 revisions

YAY, Citrus 3.0 is here!

This summary is here to share our strategy, ideas and all major changes that are part of Citrus 3.0!

Objectives

Citrus 3.0 is a major release and we want to take that as an opportunity to follow up with some improvements and refactoring that we are eager to do for quite some time. That being said we try to comply with everybody's need to migrate from older versions. People coming from Citrus 2.x should have a look at the 2.x migration guide. In addition to that we take extra care to keep breaking changes on a low-level.

Here are the main objectives we have in Citrus 3.0

Modularize Citrus

The citrus-core module is the heart of the framework and contains all capabilities that Citrus has to offer. So if you include citrus-core as a dependency in your project you will load a lot of artifacts as transitive dependencies (e.g. from Maven central). Loading that huge amount of libraries is not a good thing especially when you do not need all features provided by Citrus (e.g. Groovy script support, Xhtml, XML validation and so on).

With citrus-core it is all or nothing. So we are keen to modularize the core module into several smaller pieces. The user can then choose which of the Citrus modules and features to include into the project or even overwrite and substitute certain pieces with own implementations.

What happened to citrus-*-model modules?

Each module in former Citrus versions has had a little brother that generated model classes from XSD schema files. The XSD schemas are used for custom Spring bean definition parsing and were located in the citrus-*-model modules (e.g. citrus-config.xsd). The initial idea behind that separate model module was to separate model classes from implementations in order to use that model in a user interface called citrus-admin. With Citrus 3.x we included the XSD schemas into the implementation modules so we do not have to maintain all the citrus-*-model modules separately (also one less artifact to load, excellent).

Module categories and structure

In Citrus 3.0 we end up with following module categories:

  • Core modules

    API and base implementations of core Citrus features. There will be a separate citrus-base and citrus-spring module where latter encapsulates the Spring Framework support in Citrus (more about that in make Spring optional).

    Module Description
    citrus-api Interfaces, enums, constants
    citrus-base Default implementation of citrus-api
    citrus-spring Adds Spring Framework support to citrus-base (Bean definition parsers, Application context configuration, Autowiring in factory beans
  • Runtime modules

    Test execution modules such as JUnit, TestNG and Cucumber representing different ways to run Citrus tests.

    Module Description
    citrus-cucumber Run Citrus tests as Cucumber BDD feature files
    citrus-testng Run tests via TestNG unit test framework
    citrus-junit Run tests via JUnit4 unit test framework
    citrus-junit5 Run tests via JUnit5 unit test framework
    citrus-main Run tests via Java main CLI
    citrus-groovy Run Citrus tests as Groovy scripts (to be continued ...)
  • Endpoint modules

    Endpoints connect Citrus to a message transport like Apache Kafka, JMS, Http REST, Ftp, Mail and many more. Each endpoint may provide producer/consumer or client/server components to exchange message content over the respective transport.

    Module Description
    citrus-camel Interact with Apache Camel context, routes and control bus
    citrus-ftp Connect to and simulate FTP/SFTP servers
    citrus-http Http REST support
    citrus-jdbc Simulate JDBC drivers, connections and transactions
    citrus-jms Publish/consume messages on a JMS message broker
    citrus-kafka Exchange data via Kafka messaging
    citrus-jmx Call MBean operations and simulate MBeans
    citrus-mail Client and server side SMTP mail support
    citrus-rmi Call RMI via JNDI registry lookup and simulate RMI services
    citrus-ssh Connect to servers via SSH and simulate SSH servers
    citrus-vertx Exchange messages on the Vert.x event bus
    citrus-websocket Websocket support
    citrus-ws SOAP WebServices support including SOAP envelope handling, WSDL, WS-Security, ...
    citrus-zookeeper Connect with Zookeeper servers
    citrus-spring-integration Exchange messages on Spring Integration message channels
  • Validation modules

    When Citrus receives messages the test case is eager to verify the message content. Validation modules implement message validators and mechanisms to validate different data formats such as Json, XML, plaintext, binary content and so on. Some validation modules also add support for verification tools such as Groovy script validation, Hamcrest and AssertJ.

    Module Description
    citrus-validation-xml XML, Xpath and Xhtml message validation
    citrus-validation-json Json and JsonPath message validation
    citrus-validation-text Plain text message validation
    citrus-validation-binary Validate binary message content using input streams or base64 encoding
    citrus-validation-groovy Adds Groovy script validation for XML, Json, SQL result set
    citrus-validation-hamcrest Hamcrest matcher support like assertThat(oneOf(is(foo), is(foobar)))
  • Connector modules

    Connectors are similar to endpoints yet these components connect Citrus to a foreign technology or framework rather than implementing a message transport. Connectors typically provide a client side only implementation that enable Citrus to interact with a service or framework (e.g. Docker, Kubernetes, Selenium web driver).

    Module Description
    citrus-sql Connect with a relational database
    citrus-docker Connect with Docker deamon to manage images and containers
    citrus-selenium Connect with web driver to run web-based UI tests
    citrus-kubernetes Connect to Kubernetes cluster managing PODs services and other resources
  • Tools

    Tooling is important and the modules in this category provide little helpers and plugins for different use cases where the usage of Citrus needs to be simplified (e.g. Maven plugins, test generators, etc.)

    Module Description
    citrus-restdocs Auto generate request/response documentation for Http REST and SOAP communication
    citrus-maven-plugin Maven plugins to create tests
    citrus-archetypes Maven archetypes for project code generation
    citrus-test-generator Create and auto generate test cases (e.g. from Swagger OpenAPI specifications)
  • Catalog modules

    A catalog in Citrus combines several other modules into a set of modules that usually get used together. The citrus-core module for instance combines all available validation modules, runtimes and the Citrus Spring support into a single artifact. So the user just needs to add citrus-core to the project and can use everything Citrus has to offer (exactly like Citrus 2.x is doing).

    Module Description
    citrus-bom Bill of material holding all modules for imports
    citrus-core Default Citrus capabilities (validation, runtime, Spring support) combined into one single module (exactly the same what you have had with previous versions)
    citrus-endpoint-catalog Combine all endpoints to a single source for endpoint builders
  • Vintage modules

    We are about to take a major step in Citrus and this implies some backward incompatibilities that "vintage" modules try to solve for users that still need to stick with an older version of Citrus for some reason. With these "vintage" modules you can still run older test cases with the new Citrus 3.x code base.

    Module Description
    citrus-java-dsl Old Java DSL implementation (designer vs. runner) to be used for Citrus 2.x Java DSL tests
    citrus-arquillian Arquillian runtime for Citrus
  • Utility modules

    Module in the utility category provide tooling for internal usage only. For instance this is a shared test library that is used in unit testing by several other modules. The modules are only used when building the Citrus modules. Utility modules usually are not included in a release so they won't be pushed to Maven central.

    Module Description
    citrus-test-support Internal helper library added as test scoped dependency for unit testing in other modules. Holds shared unit testing helpers.

How to use the new module structure

Users that do not want to change much in their project regarding the dependency setup just continue to add citrus-core dependency.

<dependency>
  <groupId>com.consol.citrus</groupId>
  <artifactId>citrus-core</artifactId>
  <version>${project.version}</version>
</dependency>

This will get you the same capabilities as in Citrus 2.x with all validation modules, runtime and Spring support enabled. The citrus-core is a catalog module combining several other modules that get automatically added to your project.

The downside of this approach is that you get a lot of features and transitive dependencies that you might not need in your project. Fortunately you can exclude some features from citrus-core with the new module structure in 3.x.

<dependency>
  <groupId>com.consol.citrus</groupId>
  <artifactId>citrus-core</artifactId>
  <version>${project.version}</version>
  <exclusions>
    <exclusion>
      <groupId>com.consol.citrus</groupId>
      <artifactId>citrus-validation-groovy</artifactId>
    </exclusion>
    <exclusion>
      <groupId>com.consol.citrus</groupId>
      <artifactId>citrus-testng</artifactId>
    </exclusion>
  </exclusions>
</dependency>

The example above excludes the Groovy validation capabilities and the TestNG runtime from the project. The features will not be added to your project and less artifacts get downloaded.

Of course there is a lot more to exclude and you might end up having a more complicated configuration for all those exclusions. For people trying to operate with just what they need in their project the pull approach might be the way to go. Here you add just citrus-base as dependency.

<dependency>
  <groupId>com.consol.citrus</groupId>
  <artifactId>citrus-base</artifactId>
  <version>${project.version}</version>
</dependency>

If you want to use Spring Framework support you may also add:

<dependency>
  <groupId>com.consol.citrus</groupId>
  <artifactId>citrus-spring</artifactId>
  <version>${project.version}</version>
</dependency>

With the new modular setup in Citrus not every feature is enabled by default. As you write and execute tests in your project you might then run into errors because you are using a Citrus feature that has not yet been added to your project. Something like:

FAILURE: Caused by: NoSuchValidationMatcherException: Can not find validation matcher "assertThat" in library citrusValidationMatcherLibrary ()
	at com/consol/citrus/jms/integration/JmsTopicDurableSubscriberIT(iterate:26-48)

The error indicates that you need to add the Hamcrest validation matcher feature to the project. You can do so by adding the respective module dependency in your project:

<dependency>
  <groupId>com.consol.citrus</groupId>
  <artifactId>citrus-validation-hamcrest</artifactId>
  <version>${project.version}</version>
</dependency>

The awesomeness about it is that you can add your favorite matcher implementation as a dependency (we still need to add AssertJ support in Citrus, so we would love a contribution doing that!).

Java DSL

Citrus provides a Java domain specific language to write integration tests with a fluent API. The API makes use of the fluent builder pattern to specify test actions.

In Citrus 2.x all Java DSL related test action builders were located in a separate module called citrus-java-dsl. For better maintainability and modularization reasons the test action builders have moved into the individual modules where the test action implementation is located. In fact the Java DSL builders are now inner classes of the respective test action.

In former releases users had to choose between the two different approaches to write Java DSL tests with this fluent API:

As many things in life both approaches have individual advantages and downsides compared to each other. Citrus 3.x will only have one way to write Java test cases using one single fluent API. We have accepted the challenge to combine both approaches designer and runner into a single approach that combines the advantages and minimizes downsides.

"Vintage" Test Designer approach

The "old" designer approach has a nice fluent API that people tend to understand intuitively. Yet the designer separates test design time and runtime which leads to unexpected behavior when someone needs to mix custom code with Java DSL execution. Also debugging is not really an option as the whole test gets built first and then executed at the very end. Setting a break point at design time of the test does not really help.

"Vintage" Test Runner approach

The "old" test runner avoids the separation of design time and runtime and executes each test action immediately. This enables better debugging options and behaves like you would expect when writing custom Java code in your test. On the downside the test runner fluent API makes use of lots of lambda expressions which is not a problem in general but still many people struggle to understand the concept and the boundaries of lambdas in Java.

The new TestCaseRunner solution

In Citrus 3.x we end up using a simplified Java DSL that uses the look and feel of the former designer API but executes each step immediately to keep debugging options and the capability to add custom code between steps.

The separation between designer and runner has been removed completely. So there is only one single source of truth the TestCaseRunner and the fluent Java API for writing tests in Citrus. This simplifies the implementation in other modules (Cucumber, TestNG, JUnit) a lot.

This is how a new Java DSL test looks like in Citrus 3.x:

public class HelloServiceIT extends TestNGCitrusSpringSupport {

    @Autowired
    private HttpClient httpClient;

    @Autowired
    private KafkaEndpoint orderEvents;

    @Test
    @CitrusTest
    public void test() {
        given(variable("orderId", 1000));

        when(http().client(httpClient)
                .send()
                .post("/orders")
                .contentType(APPLICATION_FORM_URLENCODED)
                .body("order=${orderId}&name=foo"));

        then(receive(orderEvents)
                .body("Order ${orderId} has been placed"));

        and(http().client(httpClient)
                .receive()
                .response(HttpStatus.OK));
    }
}

The test extends TestNGCitrusSpringSupport. This gives you the annotation support for @CitrusTest so the test is added to the Citrus test reporting. The base class also gives you the test action execution methods given(), when(), then() and and(). This relates to the BDD Gherkin language and is widely known to a lot of people out there. If you do not want to use this BDD approach in your test you can also use the basic run() method or its shortcut version $()instead.

$(http().client(httpClient)
        .send()
        .post("/orders")
        .contentType(APPLICATION_FORM_URLENCODED)
        .body("order=${orderId}&name=foo"));

Former Citrus versions provided many base classes which confused users. The classes TestNGCitrusSupport/JUnit4CitrusSupport are now the single base class for all tests including XML and Java DSL tests.

The JUnit 5 support provides a @CitrusSupport extension annotation.

@CitrusSpringSupport
@ContextConfiguration(classes = {CitrusSpringConfig.class})
public class HelloServiceIT {

    @Autowired
    private HttpClient httpClient;

    @Autowired
    private KafkaEndpoint orderEvents;

    @Test
    @CitrusTest
    public void test(@CitrusResource GherkinTestActionRunner $) {
        $.given(variable("orderId", 1000));

        $.when(http().client(httpClient)
                .send()
                .post("/orders")
                .contentType(APPLICATION_FORM_URLENCODED)
                .body("order=${orderId}&name=foo"));

        $.then(receive(orderEvents)
                .body("Order ${orderId} has been placed"));

        $.and(http().client(httpClient)
                .receive()
                .response(HttpStatus.OK));
    }
}

TestActionBuilder

The Java DSL in Citrus consists of many actions that a user can choose from. In former Citrus versions the fluent API has been placed in a separate module. With Citrus 3.x all test actions directly provide the fluent Java builder pattern style so the implementation of all Java DSL builders have been moved from citrus-java-dsl to its individual test action classes in citrus-base.

Each TestAction implementation provides a static entry method for users to enter the fluent API builder pattern style configuration.

public class EchoAction extends AbstractTestAction {

    /** Log message */
    private final String message;

    /** Logger */
    private static Logger log = LoggerFactory.getLogger(EchoAction.class);

    /**
     * Default constructor using the builder.
     * @param builder
     */
    private EchoAction(EchoAction.Builder builder) {
        super("echo", builder);

        this.message = builder.message;
    }

    @Override
    public void doExecute(TestContext context) {
        if (message == null) {
            log.info("Citrus test " + new Date(System.currentTimeMillis()));
        } else {
            log.info(context.replaceDynamicContentInString(message));

        }
    }

    /**
     * Gets the message.
     * @return the message
     */
    public String getMessage() {
        return message;
    }

    /**
     * Action builder.
     */
    public static final class Builder extends AbstractTestActionBuilder<EchoAction, Builder> {

        private String message;

        /**
         * Fluent API action building entry method used in Java DSL.
         * @param message
         * @return
         */
        public static Builder echo(String message) {
            Builder builder = new Builder();
            builder.message(message);
            return builder;
        }

        public Builder message(String message) {
            this.message = message;
            return this;
        }

        @Override
        public EchoAction build() {
            return new EchoAction(this);
        }
    }
}

With this refactoring all test actions are now immutable and can only instantiate via the builder. You can use the action builders in the your test like this:

import com.consol.citrus.annotations.CitrusTest;
import com.consol.citrus.testng.TestNGCitrusSupport;
import org.testng.annotations.Test;

import static com.consol.citrus.actions.EchoAction.Builder.echo;

public class EchoActionJavaIT extends TestNGCitrusSupport {

    @Test
    @CitrusTest
    public void shouldEcho() {
        $(echo("Hello Citrus!"));

        $(echo("Today is citrus:currentDate()"));
    }
}

You can use static imports to add the action builders to your test.

Spring factory beans

The new test action fluent Java builder design requires us to introduce Spring factory beans that add Autowiring and connect the action builder to a bean definition parser. The factory beans live directly in the respective bean definition parser and take care on injecting dependencies to the action builder.

public class EchoActionParser implements BeanDefinitionParser {

    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        BeanDefinitionBuilder beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(EchoActionFactoryBean.class);

        DescriptionElementParser.doParse(element, beanDefinition);

        Element messageElement = DomUtils.getChildElementByTagName(element, "message");
        if (messageElement != null) {
            beanDefinition.addPropertyValue("message", DomUtils.getTextValue(messageElement));
        }

        return beanDefinition.getBeanDefinition();
    }

    /**
     * Test action factory bean.
     */
    public static class EchoActionFactoryBean extends AbstractTestActionFactoryBean<EchoAction, EchoAction.Builder> {

        private final EchoAction.Builder builder = new EchoAction.Builder();

        public void setMessage(String message) {
            builder.message(message);
        }

        @Override
        public EchoAction getObject() throws Exception {
            return builder.build();
        }

        @Override
        public Class<?> getObjectType() {
            return EchoAction.class;
        }

        /**
         * Obtains the builder.
         * @return the builder implementation.
         */
        @Override
        public EchoAction.Builder getBuilder() {
            return builder;
        }
    }
}

The factory beans can use @Autowired and bean lifecylce hooks such as InitializingBean or ApplicationContextAware. These Spring related features were moved to the factory beans. This way we can decouple citrus-api and citrus-base from Spring making it an optional library to use in Citrus.

Make Spring optional

The Spring framework is a wide spread and well appreciated framework for Java applications. The framework provides an awesome set of projects, libraries and tools. The dependency injection and IoC concepts introduced with Spring are groundbreaking.

Some people prefer to choose other approaches though to work with dependency injection. Others do struggle with mastering Citrus and Spring as new frameworks at the same time. Both frameworks Spring and Citrus are very powerful and newbies sometimes feel overwhelmed with having to deal with so much new stuff at the same time.

In former releases Citrus has been very tied to Spring and in some cases this has been a showstopper to work with Citrus for mentioned reasons.

In Citrus 3.x we make Spring optional in core modules so people can choose to enable/disable Spring support. In particular this affects the way Citrus components are started and linked to each other via the Spring application context.

Direct endpoint

By default Citrus server endpoints (e.g. Http server, Mail server, ...) are using some in memory message channel for incoming requests. This internal message channel used Spring integration as implementation. In Citrus 3.x we changed this to a custom in memory message queue implementation called DirectEndpoint. This was done to decouple Citrus core from the Spring integration library.

The DirectEndpoint lives in the citrus-base module and replaces the Spring integration message channel implementation as a default for all server endpoints.

The Spring integration message channel endpoint is not lost though. Users can still use this implementation as the endpoint was extracted from citrus-core to a separate endpoint module named citrus-spring-integration.

Citrus with Spring

When Spring is enabled for Citrus all components are loaded with a Spring application context. This enables autowiring and bean definition parsing. Latter bean definition parsing for custom components is mandatory when using XML based configuration and XML test cases in Citrus.

Users enable the Spring support in Citrus by adding:

<dependency>
  <groupId>com.consol.citrus</groupId>
  <artifactId>citrus-spring</artifactId>
  <version>${project.version}</version>
</dependency>

When using citrus-core dependency this Spring support is enabled by default in order to adjust with what has been configured in previous Citrus versions.

Citrus standalone

In case you exclude the citrus-spring module for Citrus you will load the same components and features but only without Spring framework support. Keep in mind only the XML based configuration and XML test cases continue to require Spring.

In non-Spring mode custom components can be directly configured in the Citrus context then. Also Citrus uses a resource common path lookup mechanism to identify common components that get loaded automatically. So you simply add components such as citrus-validation-json to your project classpath and the Json validation capabilities are loaded automatically.

Feel free to choose which approach fits best for your needs. Citrus running with or without Spring.

Resource path lookup

The resource path lookup is a mechanism to identify components in Citrus that should be loaded automatically when the Citrus application is started. You only need to add components to the classpath (e.g. by adding a Maven dependency) and the resource gets loaded automatically. This mechanism is used to decouple modules and to provide a non-Spring mode for Citrus.

The resource path lookup is enabled for these component types:

Type Resource Path
HeaderValidator META-INF/citrus/header/validator
MessageValidator META-INF/citrus/message/validator
ValueMatcher META-INF/citrus/value/matcher
ValidationMatcher META-INF/citrus/validation/matcher
SqlResultSetScriptValidator META-INF/citrus/sql/result-set/validator
HamcrestMatcherProvider META-INF/citrus/hamcrest/matcher/provider

Also following org.springframework.beans.factory.xml.BeanDefinitionParser add additional parsers via resource path lookup:

Parser Resource Path
TestActionRegistry META-INF/citrus/action/parser
CitrusConfigNamespaceHandler META-INF/citrus/config/parser/core
SchemaParser META-INF/citrus/schema/parser
SchemaRepositoryParser META-INF/citrus/schema-repository/parser

The bean definition parsers mentioned above are now able to dynamically lookup element parsers that live in other modules. For instance the SchemaParser loads and delegates the bean definition parsing to .xsd or .json related schema parser implementations that live in citrus-validation-xml or citrus-validation-json modules. The user needs to add these modules to the classpath when using a XML or Json schema in a schema repository.

Also the test action registry is now able to load parser implementation from other modules using the resource lookup mechanism. This way we can delegate to data dictionary parser implementations for XML or Json when they are present on the classpath.

Refactor internal integration tests

The module citrus-integration combined hundreds of internal integration tests that verified the Citrus features. The tests have been moved to its individual implementation modules. For instance XML validation related integration tests are now located in citrus-validation-xml module.

Update dependencies

It has been quite some time since the last major Citrus release. So this is a point where we catch up with all the other libraries and dependencies that evolved over time. In particular this is Apache Camel, Spring and Cucumber that all evolved with major versions in the past.

Citrus 3.0 is up to date with using the latest versions of these libraries and the following might be the most important updates:

  • Java 11
  • Spring framework 5.3
  • Apache Camel 3.9
  • TestNG 7.1
  • JUnit 5.7
  • Jetty 9.4
  • Kafka 2.8
  • Selenium 3.141
  • Log4J2 2.14
  • Cucumber 6.10

What's next!?

So 3.0 is the first version of the Citrus 3.x release train. And we are not done yet! We continue to work on our goals to simplifying the ways of writing integration tests so Citrus is ready for the future challenges of software testing.

We love to get feedback so please give it a try and tell us what you think about Citrus 3.0. Now is the time to raise your voice to improve the framework!