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 isfalse
the supplier is called every time the instance is needed. Iftrue
(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);
}