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 Isis 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 Isis' 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, the API and how to go about using the API.

Maven Configuration

Dependency Management

If your application inherits from the Apache Isis starter app (org.apache.isis.app:isis-app-starter-parent) then that will define the version automatically:

pom.xml
<parent>
    <groupId>org.apache.isis.app</groupId>
    <artifactId>isis-app-starter-parent</artifactId>
    <version>2.0.0-M6</version>
    <relativePath/>
</parent>

Alternatively, import the core BOM. This is usually done in the top-level parent pom of your application:

pom.xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.isis.core</groupId>
            <artifactId>isis-core</artifactId>
            <version>2.0.0-M6</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

In addition, add a section for the BOM of all the testing support libraries:

pom.xml
<dependencyManagement>
    <dependencies>
        <dependency>
        	<groupId>org.apache.isis.testing</groupId>
	        <artifactId>isis-testing</artifactId>
            <scope>import</scope>
            <type>pom</type>
            <version>2.0.0-M6</version>
        </dependency>
    </dependencies>
</dependencyManagement>

Dependencies

In the domain module(s) of your application, add the following dependency:

pom.xml
<dependencies>
    <dependency>
        <groupId>org.apache.isis.testing</groupId>
        <artifactId>isis-testing-fixtures-applib</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Update AppManifest

In your application’s AppManifest (top-level Spring @Configuration used to bootstrap the app), import the IsisModuleTestingFixturesApplib module:

AppManifest.java
@Configuration
@Import({
        ...
        IsisModuleTestingFixturesApplib.class,
        ...
})
public class AppManifest {
}

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, INSERTing 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:

prototyping menu

and here’s what the runFixtureScript action prompt looks like:

prompt

when this is executed, the resultant objects (actually, instances of FixtureResult`) are shown in the UI:

result list

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 isis.testing.fixtures.fixture-script-specification configuration properties. For example, here’s the configuration used by the SimpleApp starter apps:

application.yml
isis:
  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 isis.testing.fixtures.fixture-scripts-specification config properties in the configuration guide.

The actions of FixtureScripts domain service are automatically placed on the "Prototyping" menu. This can be fine-tuned using menubars.layout.xml:

menubars.layout.xml
<mb3:section>
    <mb3:named>Fixtures</mb3:named>
    <mb3:serviceAction
        objectType="isis.testing.fixtures.FixtureScripts"
        id="runFixtureScript"/>
    <mb3:serviceAction
        objectType="isis.testing.fixtures.FixtureScripts"
        id="recreateObjectsAndReturnFirst"/>
</mb3: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:

prompt specifying number

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:

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 PersonaWithBuilderScript and PersonaWithFinder interfaces are intended to be implemented typically by "persona" enums, where each enum instance captures the essential data of some persona. So, going back to the previous example, we might have:

public enum Customer_persona
        implements PersonaWithBuilderScript<..>, PersonaWithFinder<..> {

    SteveSingle("Steve", "Single", 21, MaritalStatus.SINGLE, 0)
    MeghanMarriedMum("Meghan", "Married-Mum", 35, MaritalStatus.MARRIED, 2);
    ...
}

The PersonaWithBuilderScript interface means that this enum is able to act as a factory for a BuilderScriptAbstract. This is a specialization of FixtureScript that is used to actually create the entity (customer, or whatever), using the data taken out of the enum instance:

public interface PersonaWithBuilderScript<T, F extends BuilderScriptAbstract<T,F>>  {
    F builder();
}

The PersonaWithFinder interface meanwhile indicates that the enum can "lookup" its corresponding entity from the appropriate repository domain service:

public interface PersonaWithFinder<T> {
    T findUsing(final ServiceRegistry2 serviceRegistry);

}

The SimpleApp starter app provides a sample implementation of these interfaces:

@lombok.AllArgsConstructor
public enum SimpleObject_persona
        implements PersonaWithBuilderScript<SimpleObject, SimpleObjectBuilder>,
                   PersonaWithFinder<SimpleObject> {
    FOO("Foo"),
    BAR("Bar"),
    BAZ("Baz"),
    FRODO("Frodo"),
    FROYO("Froyo"),
    FIZZ("Fizz"),
    BIP("Bip"),
    BOP("Bop"),
    BANG("Bang"),
    BOO("Boo");

    private final String name;

    @Override
    public SimpleObjectBuilder builder() {
        return new SimpleObjectBuilder().setName(name);
    }

    @Override
    public SimpleObject findUsing(final ServiceRegistry2 serviceRegistry) {
        SimpleObjectRepository simpleObjectRepository =
            serviceRegistry.lookupService(SimpleObjectRepository.class);
        return simpleObjectRepository.findByNameExact(name);
    }
}

where SimpleObjectBuilder in turn is:

import javax.inject.Inject;
import lombok.Accessors;
import lombok.Getter;
import lombok.Setter;

@Accessors(chain = true)
public class SimpleObjectBuilder
            extends BuilderScriptAbstract<SimpleObject, SimpleObjectBuilder> {

    @Getter @Setter
    private String name;                                    (1)

    @Override
    protected void execute(final ExecutionContext ec) {
        checkParam("name", ec, String.class);               (2)
        object = wrap(simpleObjects).create(name);
    }

    @Getter
    private SimpleObject object;                            (3)

    @Inject
    SimpleObjects simpleObjects;
}
1 The persona class should set this value (copied from its own state)
2 the inherited "checkParam" is used to ensure that a value is set
3 the created entity is provided as an output

Using within Tests

Fixture scripts can be called from integration tests just the same way that fixture scripts can call one another.

For example, here’s part of an integration test from the SimpleApp starter app:

SimpleObject_IntegTest.java
@Transactional
public class SimpleObject_IntegTest extends SimpleModuleIntegTestAbstract {

    SimpleObject simpleObject;

    @BeforeEach
    void setUp() {
        // given
        simpleObject = fixtureScripts.runPersona(SimpleObject_persona.FOO); (1)
    }

    public static class updateName extends SimpleObject_IntegTest {

        @Test
        void can_be_updated_directly() {

            // when
            wrap(simpleObject).updateName("new name");
            transactionService.flushTransaction();

            // then
            assertThat(wrap(simpleObject).getName()).isEqualTo("new name");
        }

        // ...
    }
}
1 runs a persona fixture script and stores the resultant domain object for testing.

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".

These builder scripts (BuilderScriptAbstract implementations) 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 UnaryOperators 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)
        );

This can then be passed in as the first argument to `SudoService’s call(…​) or run(…​) methods.

Using with Secman

TODO - explain which role needs to be setup.