-
Notifications
You must be signed in to change notification settings - Fork 137
Citrus 3.x
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!
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
- Improved Java DSL
- Make Spring optional
- Refactor internal integration tests
- Update dependencies to new major versions (Cucumber, Apache Camel, Spring Framework, ...)
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.
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).
In Citrus 3.0 we end up with following module categories:
-
API and base implementations of core Citrus features. There will be a separate
citrus-base
andcitrus-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 -
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 ...) -
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 -
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
encodingcitrus-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)))
-
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 -
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) -
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 addcitrus-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 -
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 -
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.
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!).
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.
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.
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.
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));
}
}
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.
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.
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.
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
.
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.
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.
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.
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.
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
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!