Fixture Scripts
When writing integration tests (and implementing the glue for BDD specs) it can be difficult to keep the "given" short; there could be a lot of prerequisite data that needs to exist before you can actually exercise your system. Moreover, however we do set up that data, but we also want to do so in a way that is resilient to the system changing over time.
On a very simple system you could probably get away with using SQL to insert directly into the database, or you could use a toolkit such as dbunit to upload data from flat files. Such approaches aren’t particularly maintainable though. If in the future the domain entities (and therefore corresponding database tables) change their structure, then all of those data sets will need updating.
Even more significantly, there’s no way to guarantee that the data that’s being loaded is logically consistent with the business behaviour of the domain objects themselves. That is, there’s nothing to stop your test from putting data into the database that would be invalid if one attempted to add it through the app.
The solution that Apache Causeway provides is a small library called fixture scripts. A fixture script is basically a command object for executing arbitrary work, where the work in question is almost always invoking one or more business actions. In other words, the database is populating through the functionality of the domain object model itself.
There is another benefit to Apache Causeway' fixture script approach; the fixtures can be (in prototyping mode) run from your application. This means that fixture scripts can actually help all the way through the development lifecycle:
-
when specifying a new feature, you can write a fixture script to get the system into the "given" state, and then start exploring the required functionality with the domain expert actually within the application
And if you can’t write a fixture script for the story, it probably means that there’s some prerequisite feature that needs implementing that you hadn’t previously recognized
-
when the developer implements the story, s/he has a precanned script to run when they manually verify the functionality works
-
when the developer automates the story’s acceptance test as an integration test, they already have the "given" part of the test implemented
-
when you want to pass the feature over to the QA/tester for additional manual exploratory testing, they have a fixture script to get them to a jumping off point for their explorations
-
when you want to demonstrate the implemented feature to your domain expert, your demo can use the fixture script so you don’t bore your audience in performing lots of boring setup before getting to the actual feature
-
when you want to roll out training to your users, you can write fixture scripts as part of their training exercises
The following sections explain how to setup Maven, describe the API and discuss how to mock the clock or the current user.
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>2.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
API and Usage
Fixture scripts are used to set up the system into a known state, which almost always means to populate the database. The most common use case is for integration testing, but they are also useful while prototyping/demo’ing. In both cases the system is almost always running against an in-memory database, meaning that the entire state of the system needs to be setup. As it wouldn’t be scalable to have one huge fixture script for this purpose, fixture scripts are usually organised hierarchically, with higher-level fixture scripts calling child fixture scripts that set up the individual parts the system (eg rows into a specific entity).
Fixture scripts are usually implemented by calling the business logic of the domain application.
This is preferable to, for example, INSERT
ing rows directly into database tables, because they are robust to implementation changes over time.
Fixture scripts are implemented by subclassing from the FixtureScript abstract class. In most cases you’ll want to use one of the variants provided by the framework; these are described in more detail below.
Fixture scripts are executed using the FixtureScripts domain service class.
This provides menu actions in the UI of your application (when running in prototype mode).
Typically it will only make sense for a small subset of the available fixture scripts to be exposed through the UI, for example those representing scenarios to be explored/demo’ed.
The behaviour of the FixtureScripts
domain service and the discovery of scenario fixture scripts is managed by configuration properties.
Let’s look at FixtureScripts
domain service in more detail first, then move onto exploring FixtureScript
.
FixtureScripts
The FixtureScripts domain service. This is annotated to be part of on the secondary "Prototyping" menu.
Here’s how the domain service looks like in the UI:
and here’s what the runFixtureScript
action prompt looks like:
when this is executed, the resultant objects (actually, instances of FixtureResult`) are shown in the UI:
The FixtureScripts
domain service also provides the recreateObjectsAndReturnFirst
action.
This is a convenience, saving a few clicks: it will run a nominated fixture script and return the first object created by that fixture script.
Configuration Properties
The behaviour of this domain menu service can be configured using the causeway.testing.fixtures.fixture-script-specification
configuration properties.
For example, here’s the configuration used by the SimpleApp starter apps:
causeway:
testing:
fixtures:
fixture-scripts-specification:
context-class: domainapp.webapp.application.fixture.scenarios.DomainAppDemo (1)
multiple-execution-strategy: execute (2)
run-script-default: domainapp.webapp.application.fixture.scenarios.DomainAppDemo (3)
recreate: domainapp.webapp.application.fixture.scenarios.DomainAppDemo (4)
}
}
1 | search for all fixture scripts under the package containing this class |
2 | if the same fixture script (class) is encountered more than once, then run anyway. |
3 | specify the fixture script class to provide as the default for the service’s "run fixture script" action |
4 | if present, enables a "recreate objects and return first" action to be displayed in the UI |
For more details, see causeway.testing.fixtures.fixture-scripts-specification config properties in the configuration guide.
Menubars
The actions of FixtureScripts
domain service are automatically placed on the "Prototyping" menu.
This can be fine-tuned using menubars.layout.xml
:
<mb:section>
<mb:named>Fixtures</mb:named>
<mb:serviceAction
objectType="causeway.testing.fixtures.FixtureScripts"
id="runFixtureScript"/>
<mb:serviceAction
objectType="causeway.testing.fixtures.FixtureScripts"
id="recreateObjectsAndReturnFirst"/>
</mb:section>
Let’s now look at the FixtureScript
class, where there’s a bit more to discuss.
FixtureScript
A FixtureScript is responsible for setting up the system (or more likely, one small part of the overall system) into a known state, either for prototyping or for integration testing.
The normal idiom is for the fixture script to invoke actions on business objects, in essence to replay what a real-life user would have done. That way, the fixture script will remain valid even if the underlying implementation of the system changes in the future.
For example, here’s a fixture script called RecreateSimpleObjects
.
(This used to be part of the SimpleApp starter app, though it now has a more sophisticated design, discussed below):
import lombok.Accessors;
import lombok.Getter;
import lombok.Setter;
@Accessors(chain = true)
public class RecreateSimpleObjects extends FixtureScript { (1)
public final List<String> NAMES =
Collections.unmodifiableList(Arrays.asList(
"Foo", "Bar", "Baz", "Frodo", "Froyo",
"Fizz", "Bip", "Bop", "Bang", "Boo")); (2)
public RecreateSimpleObjects() {
withDiscoverability(Discoverability.DISCOVERABLE); (3)
}
@Getter @Setter
private Integer number; (4)
@Getter
private final List<SimpleObject> simpleObjects =
Lists.newArrayList(); (5)
@Override
protected void execute(final ExecutionContext ec) { (6)
// defaults
final int number = defaultParam("number", ec, 3); (7)
// validate
if(number < 0 || number > NAMES.size()) {
throw new IllegalArgumentException(
String.format("number must be in range [0,%d)", NAMES.size()));
}
// execute
ec.executeChild(this, new SimpleObjectsTearDown()); (8)
for (int i = 0; i < number; i++) {
final SimpleObjectCreate fs =
new SimpleObjectCreate().setName(NAMES.get(i));
ec.executeChild(this, fs.getName(), fs); (9)
simpleObjects.add(fs.getSimpleObject()); (10)
}
}
}
1 | inherit from FixtureScript |
2 | a hard-coded list of values for the names. Note that the Fakedata testing module could also have been used |
3 | whether the script is "discoverable"; in other words whether it should be rendered in the drop-down by the FixtureScripts domain service |
4 | input property: the number of objects to create, up to 10; for the calling test to specify, but note this is optional and has a default (see below).
It’s important that a wrapper class is used (ie java.lang.Integer , not int ) |
5 | output property: the generated list of objects, for the calling test to grab |
6 | the mandatory execute(…) API.
The ExecutionContext parameter is discussed in more detail in the next section. |
7 | the defaultParam(…) (inherited from FixtureScript ) will default the number property (using Java’s Reflection API) if none was specified |
8 | call another fixture script (SimpleObjectsTearDown ) using the provided ExecutionContext.
There’s no need to instantiate using the FactoryService. |
9 | calling another fixture script (SimpleObjectCreate ) using the provided ExecutionContext |
10 | adding the created object to the list, for the calling object to use. |
Because this script has exposed a "number" property, it’s possible to set this from within the UI. For example:
When this is executed, the framework will parse the text and attempt to reflectively set the corresponding properties on the fixture result. So, in this case, when the fixture script is executed we actually get 6 objects created.
ExecutionContext
The ExecutionContext is passed to each FixtureScript
as it is executed.
It supports two main use cases:
-
to allow child fixture scripts to be executed, using executeChild(…) and its brethren.
This was demonstrated in the previous section
-
to read parameters obtained when the fixture script was first executed by the FixtureScripts domain service, discussed above.
The latter use case is much less frequently used, but can be helpful for example in demos, where the number of objects can be specified in the parameters
parameter of the run fixture script action.
Personas and Builders
Good integration tests are probably the best way to understand the behaviour of the domain model: better, even, than reading the code itself. This requires though that the tests are as minimal as possible so that the developer reading the test knows that everything mentioned in the test is essential to the functionality under test.
At the same time, "Persona" instances of entity classes help the developer become familiar with the data being set up. For example, "Steve Single" the Customer might be 21, single and no kids, whereas vs "Meghan Married-Mum" the Customer might be married 35 with 2 kids. Using "Steve" vs "Meghan" immediately informs the developer about the particular scenario being explored.
The Persona interfaces is intended to be implemented typically by "persona" enums, where each enum instance captures the essential data of some persona. Persona in turn unifies two lower-level interfaces, PersonaWithBuilderScript and PersonaWithFinder.
So, going back to the previous example, we might have:
@Getter
@RequiredArgsConstructor
public enum Customer_persona
implements Persona<Customer, CustomerBuilderScript> {
SteveSingle(1, "Steve", "Single", 21),
MeghanMarriedMum(2, "Meghan", "Married-Mum", 35);
private final int id;
private final String firstName;
private final String lastName;
private final int age;
@Override
public CustomerBuilderScript builder() { (1)
return new CustomerBuilderScript(this); (2)
}
@Override
public Customer findUsing(ServiceRegistry serviceRegistry) { (3)
return serviceRegistry.lookupServiceElseFail(CustomerRepository.class).findById(id).orElseThrow();
}
}
1 | from PersonaWithBuilderScript |
2 | it’s idiomatic to just pass self to the build script. |
3 | from PersonaWithFinder |
Here the CustomerBuilderScript
is a subclass of BuilderScriptAbstract, a specialized fixture script that acts as a factory of the domain object (Customer
, in this case), usig the data taken out of the enum instance.
In many cases a builder script will create a single top-level object, so the related BuilderScriptWithResult removes some boilerplate:
@RequiredArgsConstructor
public class CustomerBuilderScript extends BuilderScriptWithResult<Customer> {
private final Customer_persona persona;
@Override
protected Customer buildResult(ExecutionContext ec) {
return customerRepository.create(persona.getFirstName(), persona.getLastName(), persona.getAge());
}
@Inject CustomerRepository customerRepository;
}
Put together, the persona enums provide the "what" - hard-coded values for certain key data that the developer becomes very familiar with - while the builder provides the "how-to".
Using within a Scenario Fixture Script
With these definitions in place, the payback is that within the context of a parent fixture script, a new domain object can be easily built and retrieved later:
public class ScenarioFixtureScript extends FixtureScript {
@Override
protected void execute(ExecutionContext executionContext) {
// build it ..
Customer steve = Customer_persona.SteveSingle.build(this, executionContext);
// ... look it up
Customer steve2 = Customer_persona.SteveSingle.findUsing(serviceRegistry);
}
}
It’s also possible to use personas (or indeed any fixture scripts) from integration tests. This is discussed in the next section.
Using within Tests
Fixture scripts can be called from integration tests just the same way that fixture scripts can call one another.
Using the example persona from the previous section, we can use the FixtureScripts domain service to build the fixture.
public class Customer_IntegTest {
@Inject FixtureScripts fixtureScripts;
@Inject ServiceRegistry serviceRegistry;
@BeforeEach
public void setup() {
// build ...
Customer steve = fixtureScripts.runPersona(Customer_persona.SteveSingle); (1)
}
@Test
public void update_customer() {
// ... look it up
Customer steve = Customer_persona.SteveSingle.findUsing(serviceRegistry); (2)
}
}
1 | runs a persona fixture script |
2 | looks up the domain object. An alternative design would be to simply store the domain object as a field. |
More sophisticated use cases
Although its idiomatic for builder scripts (BuilderScriptAbstract implementations) and persona enums to come in pairs, there’s no requirement to do so; builder scripts can be used independently of the enum personas. And for more complex entity -where there might be many potential values that need to be provided - the builder script can automatically default some or even all of these values.
For example, for a customer’s date of birth, the builder could default to a date making the customer an adult, aged between 18 and 65, say. For an email address or postal address, or an image, or some "lorem ipsum" text, the Fakedata testing module could provide randomised values.
The benefit of an intelligent builder is that it further simplifies the test. The developer reading the test then knows that everything that has been specified exactly is of significance. Because non-specified values are randomised and change on each run, it also decreases the chance that the test passes "by accident" (based on some lucky hard-coded input value).
Mocking the Clock or User
The SudoService provides the mechanism to run an arbitrary block of code, but changing the effective user executing the block, or the effective time that the block was run.
This is a general purpose capability, but is especially useful for fixtures.
Mocking the Clock
It’s sometimes necessary to change the effective time that a fixture or test runs. This can be done by calling SudoService with an InteractionContext that has switched the clock.
For example:
val when = VirtualClock.nowAt(Instant.parse("2017-02-03T09:00:00.00Z")); (1)
simpleObject =
sudoService.call(
InteractionContext.switchClock(when), (2)
()-> fixtureScripts.runPersona(SimpleObject_persona.FOO) (3)
);
1 | the effective time, in other words the date/time we want the ClockService to report |
2 | Unary operator on InteractionContext to switch the effective time |
3 | the block of code to run within this modified InteractionContext. |
Switching the User ("Sudo")
Sometimes in our fixture scripts we want to perform a business action running as a particular user.
This might be for the usual reason - so that our fixtures accurately reflect the reality of the system with all business constraints enforced using the WrapperFactory
- or more straightforwardly it might be simply that the action depends on the identity of the user invoking the action.
Either way, this can be done by calling SudoService with an InteractionContext that has switched the effective user.
For example:
val who = UserMemento.ofName("fred"); (1)
simpleObject =
sudoService.call(
InteractionContext.switchUser(who), (2)
()-> fixtureScripts.runPersona(SimpleObject_persona.FOO) (3)
);
1 | the effective user, in other words the user that we want UserService to report |
2 | Unary operator on InteractionContext to switch the effective user |
3 | the block of code to run within this modified InteractionContext. |
Mocking the Clock and the User
If we want to change both the effective time and the effective user, then we just need to combine the UnaryOperator
s passed into SudoService.
As a convenience, InteractionContext provides a helper method for just this purpose:
For example:
val who = UserMemento.ofName("fred");
val when = VirtualClock.nowAt(Instant.parse("2017-02-03T09:00:00.00Z"));
val switchWhoAndWhen =
InteractionContext.combine(
InteractionContext.switchUser(who),
InteractionContext.switchClock(when)
);