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

API and Usage

There are two main parts to using fixture scripts: the FixtureScripts domain service class, and the FixtureScript view model class:

  • The role of the FixtureScripts domain service is to locate all fixture scripts from the classpath and to let them be invoked, either from an integration test/BDD spec or from the UI of your Isis app.

  • The role of FixtureScript meanwhile is to subclass for each of the scenarios that you want to define. You can also subclass from FixtureScript to create helpers; more on this below.

Let’s look at FixtureScripts domain service in more detail first.

FixtureScripts

The framework provides a default implementation of FixtureScripts domain service, namely the FixtureScriptsDefault domain service. This is annotated to be rendered on the secondary "Prototyping" menu.

The behaviour of this domain menu service can be refined by providing an implementation of the optional FixtureScriptsSpecificationProvider SPI.

For example, here’s the FixtureScriptsSpecificationProvider service that’s generated by the SimpleApp starter apps:

@DomainService( nature = NatureOfService.DOMAIN )
public class DomainAppFixtureScriptsSpecificationProvider
                    implements FixtureScriptsSpecificationProvider {
    public FixtureScriptsSpecification getSpecification() {
        return FixtureScriptsSpecification
            .builder(DomainAppFixtureScriptsSpecificationProvider.class)               (1)
            .with(FixtureScripts.MultipleExecutionStrategy.EXECUTE)                    (2)
            .withRunScriptDefault(DomainAppDemo.class)                                 (3)
            .withRunScriptDropDown(FixtureScriptsSpecification.DropDownPolicy.CHOICES) (4)
            .withRecreate(DomainAppDemo.class)                                         (5)
            .build();
    }
}
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; more on this in Organizing Fixture scripts, below.
3 specify the fixture script class to provide as the default for the service’s "run fixture script" action
4 whether the service’s "run fixture script" action should display other fixture scripts using a choices drop down or (if there are very many of them) using an auto-complete
5 if present, enables a "recreate objects and return first" action to be displayed in the UI

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

If you had defined many fixture scripts then a drop-down might become unwieldy, in which case your code would probably override the autoComplete…​()) instead:

    @Override
    public List<FixtureScript> autoComplete0RunFixtureScript(final @MinLength(1) String searchArg) {
        return super.autoComplete0RunFixtureScript(searchArg);
    }

You are free, of course, to add additional "convenience" actions into it if you wish for the most commonly used/demo’d setups ; you’ll find that the SimpleApp archetype adds this additional action:

    @Action(
            restrictTo = RestrictTo.PROTOTYPING
    )
    @ActionLayout(
            cssClassFa="fa fa-refresh"
    )
    @MemberOrder(sequence="20")
    public Object recreateObjectsAndReturnFirst() {
        final List<FixtureResult> run = findFixtureScriptFor(RecreateSimpleObjects.class).run(null);
        return run.get(0).getObject();
    }

Let’s now look at the FixtureScript class, where there’s a bit more going on.

FixtureScript

A fixture script is ultimately just a block of code that can be executed, so it’s up to you how you implement it to set up the system. However, we strongly recommend that you use it 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 org.apache.isis.applib.fixturescripts.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 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
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. Note that although the fixture script is a view model, it’s fine to simply instantiate it (rather than using FactoryService#create(…​)).
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.

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 an integration test from the SimpleApp starter app:

public class SimpleObjectIntegTest extends SimpleAppIntegTest {
    SimpleObject simpleObjectWrapped;
    @Before
    public void setUp() throws Exception {
        // given
        RecreateSimpleObjects fs =
             new RecreateSimpleObjects().setNumber(1);  (1)
        fixtureScripts.runFixtureScript(fs, null);      (2)

        SimpleObject simpleObjectPojo =
            fs.getSimpleObjects().get(0);               (3)
        assertThat(simpleObjectPojo).isNotNull();

        simpleObjectWrapped = wrap(simpleObjectPojo);   (4)
    }
    @Test
    public void accessible() throws Exception {
        // when
        final String name = simpleObjectWrapped.getName();
        // then
        assertThat(name).isEqualTo(fs.NAMES.get(0));
    }
    ...
    @Inject
    FixtureScripts fixtureScripts;                      (5)
}
1 instantiate the fixture script for this test, and configure
2 execute the fixture script
3 obtain the object under test from the fixture
4 wrap the object (to simulate being interacted with through the UI)
5 inject the FixtureScripts domain service (just like any other domain service)

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

This simplifies the integration tests considerably:

public class SimpleObject_IntegTest extends SimpleModuleIntegTestAbstract {

    SimpleObject simpleObject;

    @Before
    public void setUp() {
        // given
        simpleObject = fixtureScripts.runBuilderScript(SimpleObject_persona.FOO.builder());
    }

    @Test
    public void accessible() {
        // when
        final String name = wrap(simpleObject).getName();

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

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 buider 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

It’s often useful to be able to change the effective time that a test runs. The framework itself uses the ClockService to obtain the current time, and applications should also use this domain service for a consistent view of the current time.

This page describes how to change the current time, and how to set up tests using fixture scripts with a mocked time.

FixtureClock

The default ClockService implementation in fact simply delegates to another class defined in the API, namely the o.a.i.applib.clock.Clock, an abstract singleton class. It is not recommended that your code use the Clock directly, but it’s worth understanding how this all works.

  • If running in production (server) mode, then the framework will (lazily) instantiate the `SystemClock when first required. This is a read-only clock that reads from the system time. The instance registers itself as the singleton and cannot be replaced.

  • If running in prototype mode, though, then the framework (using FixturesLifecycleService) will instead instantiate FixtureClock. This is a read-write clock that will behave as the system clock, unless it is explicitly set using eg, FixtureClock#setDate(…​) or FixtureClock#setTime(…​) etc.

    TODO: v2 - looking now to remove Clock singleton, so refactor FixtureClock and TickingFixtureClock with alternative implementations of ClockService.

Moreover, the FixtureClock singleton can be replaced with another implementation. And, it is sometimes useful to replace it using TickingFixtureClock, a subclass that is very similar to FixtureClock (in that the time can be changed) but which will continue to tick once set.

TODO: v2 - this is currently broken; there is nothing that actually instantiates and installs the TickingFixtureClock. Will be superceded as a requirement if refactor to provide alternative ClockService implementations instead.

TickingClockFixture

The TickingClockFixture is a pre-built fixture script that resets the date/time returned by the ClockService to a known value.

Thereafter the time returned continues to tick forward (as would the real clock) until reset once more.

For example, to set the clock to return "3 Sept 2014", use:

executionContext.executeChild(this, new TickingClockFixture().setTo("2014-09-03"));

A variety of format strings are supported; the format "YYYY-MM-DD" (as shown above) will work in every locale.

This fixture script requires that a TickingFixtureClock is initialized during bootstrapping.

SudoService

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.

For example, consider this code that creates a todo item:

Production code that depends on current user
// annotations omitted for clarity
public ToDoItem newToDo(
        final String description,
        final Category category,
        final Subcategory subcategory,
        final LocalDate dueBy,
        final BigDecimal cost) {
    return newToDo(description, category, subcategory, currentUserName(), dueBy, cost);
}
private String currentUserName() {
    return container.getUser().getName();       (1)
}
1 is the current user.

The fixture for this can use the SudoService to run a block of code as a specified user:

Fixture Script
final String description = ...
final Category category = ...
final Subcategory subcategory = ...
final LocalDate dueBy = ...
final BigDecimal cost = ...
final Location location = ...

toDoItem = sudoService.sudo(username,
        new Callable<ToDoItem>() {
            @Override
            public ToDoItem call() {
                final ToDoItem toDoItem = wrap(toDoItems).newToDo(
                        description, category, subcategory, dueBy, cost);
                wrap(toDoItem).setLocation(location);
                return toDoItem;
            }
        });

Behind the scenes the SudoService simply talks to the UserService to override the user returned by the getUser() API. It is possible to override both users and roles.