This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.

Unit testing

Note

This is mostly a direct copy from an old package-info.java file from the ch.tocco.nice2.persist.core.test.inject package. Feel free to rewrite or expand this documentation.

Inject Test Objects

Classes in this package (and sub package) aim to provide a means to inject dependencies into test cases. If the fixture objects get more complex, it is a pain to create all necessary objects using new. This tool helps in creating instances from a configured set of bindings just like it is done in Guice. Maybe it is replaced by guice later.

Usage

Let’s start with an example test case.

public class MyTestCase extends AbstractInjectingTestCase {

    @Resource
    private MySpecialDataModel dataModel;

    @Resource
    private Context context;

    @OnTest @Resource(shareable = false)
    private QueryBuilderFactory queryBuilderFactory;

    @Override
    protected void setupTestModules() {
        install(FixtureModules.embeddedDbModules(false));
        bindDataModel(MySpecialDataModel.class);
    }

    @Test
    public void testHello() throws Exception {
        Context.Tx tx = context.beginTx();
        try {
            Entity e = context.getEntityManager(dataModel.address.getName()).create();
            e.setValue(dataModel.address.firstname.name(), "Lars");
            e.setValue(dataModel.address.lastname.name(), "Muillere");
            tx.commit();
        } finally {
            tx.finish();
        }
        Query q = queryBuilderFactory.find(dataModel.address.getName())
            .where(Conditions.field(dataModel.address.firstname.name()).like("*ars"))
            .build(context);
        Assert.assertNotNull(q);
        EntityList list = q.execute();
        Assert.assertEquals(list.size(), 1);
        Entity e = list.get(0);
        Assert.assertEquals(e.getString(dataModel.address.firstname.name()), "Lars");
        Assert.assertEquals(e.getString(dataModel.address.lastname.name()), "Muillere");
        Assert.assertNotNull(list);
    }

    public static class MySpecialDataModel extends AbstractDataModel {
        //left out for brevity
    }
}

The “meat” is in BaseInjectingTestCase and in the modules that are returned setupTestModules(). The modules specify which objects to create and the super class has a FixtureInjectListener test listener configured, that inspects the test instance and injects all dependencies (annotated with @Resource).

Supplier as Factories

All objects are created via factories for certain types. For example, the class DefaultTypeManagerSupplier creates a TypeManager. It implements the Supplier interface. At the end every object is created using a supplier like that.

A binding is a mapping from a (optionally named) type to a supplier. When an object is requested, the map of bindings is searched for a matching supplier. Then a recursive injection starts by injecting dependencies into the supplier. After that the instance is returned and made available in the test case. If any object has to be created, the best constructor is used where all its parameters are available in the binding map.

Note that every test class has its own unique set of bindings such that objects that are injected are created for every test class.

If there are circular dependencies this is not recognized right now and results in an StackOverflowError. The workaround is to explicitely create a lazy binding for one of the bindings.

class AImpl implements A {
    public A(B b) { ... }
}
class BImpl implements B {
    public B(A a) { ... }
}

Since A needs B and B needs A there is no way to create any instance eagerly. One binding must be declared lazy. A proxy will be created that will create the “real” instance on first access.

bindLazy(A.class, AImpl.class);
bind(B.class, BImpl.class);

See LazyInjectTest for a complete example.

FixtureModule

A fixture module is meant to group a set of bindings. It uses a TypeAwareTestModuleBinder to add bindings and can also take some action when the test suite is finished. There are some modules for setting up the persist layer.

Contributions: Collection Bindings

It is possible to bind a List or Set of objects. Use the corresponding bind methods on TypeAwareTestModuleBinder. The list or set can be injected like any other type. The set or list is created once for the first binding. Other bindings contribute to the list/set, thus other modules can simply add new elements to a list or set without touching other code.

Overwrite Bindings

Bindings are added to a map in the order they are defined in the modules. Thus, to use a different binding than configured, just add a new binding at the end which will overwrite any previously added binding. The abstract test class AbstractInjectingTestCase defines some shortcut methods to conveniently add bindings from within setupTestModules()

For example, the L10N interface is bound by default to some testing instance that simply returns some default values. If you wanted to use a different implementation everywhere, bind another one at the end of setupTestModules()

protected void setupTestModules() {
    install(FixtureModules.embeddedDbModules(false));
    install(TestConfigs.persistHistory());
    bindDataModel(EventDataModel.class);
    //use MyL10N instead
    bind(L10N.class, MyL10N.class)
}

Injections

In order to receive any injection you must annotate the field or setter with the @Resource annotation. This annotation allows to specify more properties:

  • shareable if this is false the supplier is called every time the instance is needed. If true (which is the default), the instance is cached simulating a singleton scope.

  • name: if this is set, the supplier is looked up with the type and the given name.

Other properties of the annotation are not supported.

By default, all dependencies are injected at suite start and are thus singletons with respect to the test execution (all test methods share this instance). Alternatively, you can use the @OnTest annotation if you want to inject the dependency before each test method. This makes obviously only sense, if shareable = false, such that the instance is not cached.

Persist Module and the DataModel

Every persist module in FixtureModules requires a DataModel to work. There is a helper method for binding DataModel in AbstractInjectingTestCase that will bind the given model to both interfaces: NiceDataModel and DataModel to ensure that services get the same data model injected no matter which interface they declare in their constructor signatures. Of course, the given data model must implement both interfaces for this to work.

A data model can be created with a fluent-style api:

public static class MySpecialDataModel extends AbstractDataModel {

    public final EntityModelMock addressType = entityModel("Address_type")
        .with(field("unique_id").unique())
        .with(standardPk())
        .with(field("label"));

    public final EntityModelMock address = entityModel("Address")
        .with(standardPk())
        .with(field("firstname"))
        .with(field("lastname").nullable())
        .withManyToOne(addressType);

    public MySpecialDataModel(TypeManager typeManager, PersistenceBackend backend) {
        super(typeManager, backend);
        commit();
    }
}

This is creating two connected entity models. If you define this model in your test, you can you refer to the models by their field name. If you also want to refer to the field models like that, use a different notation:

public static class MySpecialDataModel extends AbstractDataModel {

    public final AddressType addressType = new AddressType(this);
    public final Address address = new Address(this);

    public MySpecialDataModel(TypeManager typeManager, PersistenceBackend backend) {
        super(typeManager, backend);
        address.manyToOne(addressType);
        commit();
    }

    public static class AddressType extends EntityModelMock {
        public final FieldModelMock pk = field("pk").asSerial().key();
        public final FieldModelMock uniqueId = field("unique_id").unique();
        public final FieldModelMock label = field("label");

        AddressType(AbstractDataModel model) {
            super("Address_type", model);
        }
    }
    public static class Address extends EntityModelMock {
        public final FieldModelMock pk = field("pk").asSerial().key();
        public final FieldModelMock firstname = field("firstname");
        public final FieldModelMock lastname = field("lastname").nullable();

        Address(AbstractDataModel model) {
            super("Address", model);
        }
    }
}

Then bind it using the bindDataModel method:

protected void setupTestModules() {
    ...
    bindDataModel(MySpecialDataModel.class);
}