Integration Test Support
Apache Causeway builds on top of Spring Boot’s integration testing support, in particular using @SpringBootTest. This configures and bootstraps the Apache Causeway runtime, usually running against an in-memory database.
On top of that, we usually:
-
use fixture scripts to set up the app’s initial state
-
use the WrapperFactory to simulate the UI.
To explain further, let’s walk through of the SimpleApp starter app.
Maven Configuration
Dependency Management
If your application inherits from the Apache Causeway starter app (org.apache.causeway.app:causeway-app-starter-parent
) then that will define the version automatically:
<parent>
<groupId>org.apache.causeway.app</groupId>
<artifactId>causeway-app-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
Alternatively, import the core BOM. This is usually done in the top-level parent pom of your application:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.causeway.core</groupId>
<artifactId>causeway-core</artifactId>
<version>3.1.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
In addition, add an entry for the BOM of all the testing support libraries:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.causeway.testing</groupId>
<artifactId>causeway-testing</artifactId>
<scope>import</scope>
<type>pom</type>
<version>3.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Dependencies
In the domain module(s) of your application, add the following dependency:
<dependencies>
<dependency>
<groupId>org.apache.causeway.testing</groupId>
<artifactId>causeway-testing-integtestsupport-applib</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
As a convenience, this dependency also brings in the Fakedata libraries. If not required this can always be explicitly excluded.
We also highly recommend using Fixture Scripts to manage the setup of the "given" state:
<dependencies>
<dependency>
<groupId>org.apache.causeway.testing</groupId>
<artifactId>causeway-testing-fixtures-applib</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
The Parent POM configures the Maven surefire plugin in three separate executions for unit tests, integ tests and for BDD specs. This relies on a naming convention:
Classes named Not following this convention (intermixing unit tests with integ tests) may cause the latter to fail. |
Update AppManifest
In your application’s AppManifest
(top-level Spring @Configuration
used to bootstrap the app), import the CausewayModuleTestingIntegTestSupportApplib
module:
@Configuration
@Import({
...
CausewayModuleTestingIntegTestApplib.class,
...
})
public class AppManifest {
}
Bootstrapping
We typically put the bootstrapping of Apache Causeway into a superclass, one for each Maven module. This allows an individual "slice" of the application to be tested, rather than as a monolith.
So, for the module-simple
module, we have:
@SpringBootTest(
classes = SimpleModuleIntegTestAbstract.AppManifest.class
)
@TestPropertySource({
CausewayPresets.H2InMemory_withUniqueSchema,
CausewayPresets.DataNucleusAutoCreate,
CausewayPresets.UseLog4j2Test,
})
public abstract class SimpleModuleIntegTestAbstract extends CausewayIntegrationTestAbstractWithFixtures {
@Configuration
@Import({
CausewayModuleCoreRuntimeServices.class,
CausewayModuleSecurityBypass.class,
CausewayModuleJdoDataNucleus5.class,
CausewayModuleTestingFixturesApplib.class,
SimpleModule.class (1)
})
public static class AppManifest {
}
}
1 | references just the SimpleModule |
while in the webapp
module we have:
@SpringBootTest(
classes = ApplicationIntegTestAbstract.AppManifest.class
)
@TestPropertySource({
CausewayPresets.H2InMemory_withUniqueSchema,
CausewayPresets.DataNucleusAutoCreate,
CausewayPresets.UseLog4j2Test,
})
@ContextConfiguration
public abstract class ApplicationIntegTestAbstract extends CausewayIntegrationTestAbstract {
@Configuration
@Import({
CausewayModuleCoreRuntimeServices.class,
CausewayModuleJdoDataNucleus5.class,
CausewayModuleSecurityBypass.class,
CausewayModuleTestingFixturesApplib.class,
ApplicationModule.class (1)
})
public static class AppManifest {
}
}
1 | references the top-level ApplicationModule |
You can see that these are very similar, what’s different is the module referenced.
They both also force an in-memory database, with JDO/DataNucleus ORM configured to autocreate the database schema.
Faster bootstrapping
By default integration tests are run in "production" deployment mode.
With the causeway.core.meta-model.introspector.mode=
set to its default value, this results in full introspection of the Apache Causeway metamodel.
While this does have the benefit that the metamodel will be validated, the downside is that the test takes longer to bootstrap.
To bootstrap lazily, set the property to 'lazy'. This can be done by adding:
@TestPropertySource(CausewayPresets.IntrospectLazily)
Typical Usage
Let’s now drill down and look at SimpleObject_IntegTest
, which (not surprisingly) is the integration test that exercises the SimpleObject
class:
@Transactional (1)
public class SimpleObject_IntegTest extends SimpleModuleIntegTestAbstract { (2)
SimpleObject simpleObject;
@BeforeEach (3)
public void setUp() {
// given
simpleObject = fixtureScripts.runPersona(SimpleObject_persona.FOO); (4)
}
//...
}
1 | The Spring @Transactional annotation means that any changes made in the database will be rolled back automatically. There’s further discussion below. |
2 | inherits from the module-level abstract class (which in turn will be annotated with @SpringBootTest , see bootstrapping earlier). |
3 | uses JUnit 5 |
4 | uses a fixture script to setup the database.
The FixtureScripts domain service is inherited from the superclass. |
Testing a property
For example, let’s test the SimpleObject#name
property:
import lombok.Getter;
import lombok.Setter;
@DomainObject()
public class SimpleObject
...
@Getter @Setter
@Name private String name;
...
}
Properties are visible but unmodifiable by default We therefore have two tests for each of these facts.
Non-editable properties is change from Causeway v1.x.
Use the causeway.applib.annotation.domain-object.editing configuration property to pverride.
|
By convention, we group tests into nested static classes, so the corresponding integration test is:
public class SimpleObject_IntegTest extends SimpleModuleIntegTestAbstract {
...
public static class name extends SimpleObject_IntegTest {
@Test
void accessible() {
// when
final String name = wrap(simpleObject).getName(); (1)
// then
assertThat(name).isEqualTo(simpleObject.getName()); (2)
}
@Test
void not_editable() {
// expect
assertThatThrownBy(()->{ (3)
// when
wrap(simpleObject).setName("new name");
}).isInstanceOf(DisabledException.class);
}
}
}
1 | we use the WrapperFactory to access the domain object.
This will check that property (for access) is visible to the current user. |
2 | we use AssertJ for assertions. |
3 | attempting to set the name on the property (through the wrapper) is disallowed.
We detect this by catching a DisabledException (using AssertJ's assertThatThrownBy(…) ). |
Testing an action
The way that the simple app allows the name to be updated is through an updateName
action:
@DomainObject()
public class SimpleObject
...
public static class UpdateNameActionDomainEvent extends SimpleObject.ActionDomainEvent {}
@Action(...
domainEvent = UpdateNameActionDomainEvent.class) (1)
public SimpleObject updateName(
@Name final String name) { (2)
setName(name); (3)
return this;
}
public String default0UpdateName() { (4)
return getName();
}
...
}
1 | an event will be emitted whenever the action is interacted with |
2 | (the @Name annotation is discussed later) |
3 | the business functionality to be tested. |
4 | (we normally test default methods using unit tests). |
The corresponding integration test is:
public class SimpleObject_IntegTest extends SimpleModuleIntegTestAbstract {
...
public static class updateName extends SimpleObject_IntegTest {
@DomainService (1)
public static class UpdateNameListener {
@Getter
List<SimpleObject.UpdateNameActionDomainEvent> events = new ArrayList<>();
@EventListener(SimpleObject.UpdateNameActionDomainEvent.class)
public void on(SimpleObject.UpdateNameActionDomainEvent ev) {
events.add(ev);
}
}
@Inject
UpdateNameListener updateNameListener;
@Test
public void can_be_updated_directly() {
// given
updateNameListener.getEvents().clear();
// when
wrap(simpleObject).updateName("new name"); (2)
transactionService.flushTransaction();
// then
assertThat(wrap(simpleObject).getName()).isEqualTo("new name"); (3)
assertThat(updateNameListener.getEvents()).hasSize(5); (4)
}
}
1 | defines a domain service to detect the domain events emitted when interacting with the action (through the wrapper).
This domain service is not part of the production code base, but is detected automatically thanks to |
2 | interact with the action through the wrapper |
3 | verify the object was updated correctly |
4 | verify that the domain events were emitted.
Since this was a successful interaction, there will have been 5 events, one for each of the event phases (hide, disable, validate, executing, executed) |
Testing a type meta-annotation
As was pointed out earlier, the name
parameter to updateName()
has the @Name
meta-annotation.
This meta-annotation also defines some business rules, through @Property
and @Parameter
:
@Property(mustSatisfy = Name.NoExclamationMarks.class, maxLength = Name.MAX_LEN)
@Parameter(mustSatisfy = Name.NoExclamationMarks.class, maxLength = Name.MAX_LEN)
@ParameterLayout(named = "Name")
...
public @interface Name {
int MAX_LEN = 40;
class NoExclamationMarks extends AbstractSpecification2<String> {
@Override
public TranslatableString satisfiesTranslatableSafely(final String name) {
return name != null && name.contains("!")
? TranslatableString.tr("Exclamation mark is not allowed")
: null;
}
}
}
We test this by checking the wrapper throws an InvalidException
:
public class SimpleObject_IntegTest extends SimpleModuleIntegTestAbstract {
...
public static class updateName extends SimpleObject_IntegTest {
...
@Test
public void failsValidation() {
// expect
InvalidException cause = assertThrows(InvalidException.class, ()->{
// when
wrap(simpleObject).updateName("new name!");
});
// then
assertThat(cause.getMessage(), containsString("Exclamation mark is not allowed."));
}
}
Some of the methods being called here are inherited from the superclass, so let’s look at that next.
CausewayIntegrationTestAbstract / CausewayIntegrationGwtAbstract
For convenience the framework provides the CausewayIntegrationTestAbstract and CausewayIntegrationGwtAbstract classes, either of which can be used as a base class for integration tests.
These classes that provides the wrap(…)
method (as see in the previous section).
It also provides a number of other convenience methods:
-
wrap()
- as just mentioned. Also has an alias ofw()
. -
unwrap()
- to unwrap. -
mixin()
- to instantiate a mixin around a domain object. Also has an alias ofm()
. -
wrapMixin()
- which naturally enoughwrap()
s the result of amixin()
. This has an alias ofwm()
.
This class also provides a number of injected domain services:
-
to query and persist objects through a generic repository
-
for instantiating domain objects and mixins
-
to acces the current user
-
to access domain services
-
to access the metamodel.
-
to simulate interactions through the UI. This is discussed further below.
-
for more control over transactions
Wrapper Factory
The WrapperFactory service is responsible for wrapping a domain object in a dynamic proxy, of the same type as the object being proxied. The role of this wrapper is to simulate the UI.
It does this by allowing through method invocations that would be allowed if the user were interacting with the domain object through one of the viewers, but throwing an exception if the user attempts to interact with the domain object in a way that would not be possible if using the UI.
The WrapperFactory
uses ByteBuddy to perform its magic.
The mechanics are as follows:
-
the integration test calls the
WrapperFactory
to obtain a wrapper for the domain object under test. This is usually done in the test’ssetUp()
method. -
the test calls the methods on the wrapper rather than the domain object itself
-
the wrapper performs a reverse lookup from the method invoked (a regular
java.lang.reflect.Method
instance) into the Apache Causeway metamodel -
(like a viewer), the wrapper then performs the "see it/use it/do it" checks, checking that the member is visible, that it is enabled and (if there are arguments) that the arguments are valid
-
if the business rule checks pass, then the underlying member is invoked. Otherwise an exception is thrown.
The type of exception depends upon what sort of check failed.
It’s straightforward enough: if the member is invisible then a HiddenException
is thrown; if it’s not usable then you’ll get a DisabledException
, if the args are not valid then catch an InvalidException
.
Wrapping and Unwrapping
Wrapping a domain object is very straightforward; simply call WrapperFactory#wrap(…)
.
For example:
Customer customer = ...;
Customer wrappedCustomer = wrapperFactory.wrap(wrappedCustomer);
Going the other way — getting hold of the underlying (wrapped) domain object — is just as easy; just call WrapperFactory#unwrap(…)
.
For example:
Customer wrappedCustomer = ...;
Customer customer = wrapperFactory.unwrap(wrappedCustomer);
If you prefer, you also can get the underlying object from the wrapper itself, by downcasting to WrappingObject
and calling __causeway_wrapped()
method:
Customer wrappedCustomer = ...;
Customer customer = (Customer)((WrappingObject)wrappedCustomer).__causeway_wrapped());
We’re not sure that’s any easier (in fact we’re certain it looks rather obscure). Stick with calling unwrap(…)
!
Using the wrapper
As the wrapper is intended to simulate the UI, only those methods that correspond to the "primary" methods of the domain object’s members are allowed to be called. That means:
-
for object properties the test can call the getter or setter method
-
for object collections the test can call the getter to access the contents.
For this to work properly, the collection should be marked as read-only, generally globally using the causeway.applib.annotation.domain-object.editing
configuration property. -
for object actions the test can call the action method itself.
As a convenience, we also allow the test to call any default…()
,choices…()
or autoComplete…()
method.
These are often useful for obtaining a valid value to use.
What the test can’t call is any of the remaining supporting methods, such as hide…()
, disable…()
or validate…()
.
That’s because their value is implied by the exception being thrown.
The wrapper does also allow the object’s title()
method or its toString()
, however this is little use for objects whose title is built up using the @Title
annotation.
Instead, we recommend that your test verifies an object’s title by calling TitleService#titleOf(…)
method.
Firing Domain Events
As well as enforcing business rules, the wrapper has another important feature, namely that it will cause domain events to be fired.
The walk through of SimpleApp above showed how this works.
Manual Teardown
The Spring @Transactional annotation means that any changes made in the database will be rolled back automatically. Normally this is what we want. For more on this topic, see the Spring docs.
Sometimes you may want to commit to the database to verify state, eg using Spring’s @Commit annotation. In these cases, you’ll need to manually tear down the database contents afterwards, in readiness for the next test.
This can be done using the fixture library’s ModuleWithFixtures
interface:
public interface ModuleWithFixtures {
default FixtureScript getRefDataSetupFixture() { (1)
return FixtureScript.NOOP;
}
default FixtureScript getTeardownFixture() { (2)
return FixtureScript.NOOP;
}
}
1 | Optionally each module can define a FixtureScript which holds immutable "reference data". |
2 | Optionally each module can define a tear-down FixtureScript , used to remove the contents of all of the operational/transactional entities (but ignoring reference data fixtures). |
This should be implemented by module classes, eg as is the case for SimpleModule
.
@Configuration
@Import({})
@ComponentScan
public class SimpleModule implements ModuleWithFixtures {
@Override
public FixtureScript getTeardownFixture() {
return new TeardownFixtureAbstract() {
@Override
protected void execute(ExecutionContext executionContext) {
deleteFrom(SimpleObject.class);
}
};
}
...
}
The ModuleWithFixturesService
aggregates all these setup and teardown fixtures, honouring the module dependencies:
@Service
public class ModuleWithFixturesService {
...
public FixtureScript getRefDataSetupFixture() { ... }
public FixtureScript getTeardownFixture() { ... }
...
}
Thus, the setup fixture runs the setup for the "inner-most" "leaf-level" modules with no dependencies first, whereas the teardown fixture runs in the opposite order, with the "outer-most" "top-level" modules torn down first.
Hints-n-Tips
Using JDO/DataNucleus
When running integration tests through the IDE, and if using the JDO/DataNucleus ORM, then make sure the module(s) with entities have been enhanced first. If using IntelliJ, we recommend creating a run Maven configuration that runs |