Achieving Stateless Transactional Testing
The Case Study addresses the problem of stateless testing of transactional
operations by introducing compensating transactions.
So, at the beginning of the test, objects on which there are dependencies
are inserted transactionally into the database,
the transacational operations are executed,
and then at the conclusion of the test,
all objects created by the test are removed transactionally.
Here is the important code from
PetStoreImplTransactionTest
.
PetStoreImplTransactionTest.java (excerpt)
public class PetStoreImplTransactionTest extends SpringUnitTest {
protected void onSetUp() throws Exception {
Map<String, List<? extends Object>> prepopulated = (Map<String, List<? extends Object>>)getObject("prepopulated");
if (prepopulated != null && prepopulated.size() > 0) {
List<? extends Object> categories = prepopulated.get("categories");
if (categories != null) {
getAdministrationService().createCategories((List<? extends Category>)categories);
}
List<? extends Object> accounts = prepopulated.get("accounts");
if (accounts != null) {
getAdministrationService().createAccounts((List<? extends Account>)accounts);
}
List<? extends Object> products = prepopulated.get("products");
if (products != null) {
getAdministrationService().createProducts((List<? extends Product>)products);
}
List<? extends Object> items = prepopulated.get("items");
if (items != null) {
getAdministrationService().createItems((List<? extends Item>)items);
}
List<? extends Object> orders = prepopulated.get("orders");
if (orders != null) {
getAdministrationService().createOrders((List<? extends Order>)orders);
}
}
}
protected void onTearDown() throws Exception {
getAdministrationService().deleteOrders();
getAdministrationService().deleteItems();
getAdministrationService().deleteProducts();
getAdministrationService().deleteAccounts();
getAdministrationService().deleteCategories();
setDirty();
}
}
To begin with, notice that this test extends
SpringUnitTest
,
not SpringUnitTransactionalTest
.
Allowing that perhaps the choice of class name is open to question,
SpringUnitTransactionalTest
rolls back transactions,
which is not the desired behavior here.
Remember also that the methods intended to be overriden by subclasses
are
onSetUp
and
onTearDown
.
The Case Study uses
onSetUp
to create all
objects needed to seed the database.
Notice that it uses methods provided by the
AdministrationService
to accomplish this.
onTearDown
is called after the body of the test is completed,
and here, the Case Study restores the original database state
by calling the
AdministrationService
to delete objects.
The call to
setDirty
at the conclusion of
onTearDown
illustrates a fairly subtle interaction between Spring and JUnit.
In Spring version 1.x, there are two object scopes, singleton and prototype.
Prototype scope is appropriate for stateless objects, singleton scope
is appropriate for stateful, shared objects.
For unit tests, the SpringUnit context is itself a singleton bean
that contains a map holding various test data inputs and expected values.
The Spring framework avoids rebuilding this structure with each
JUnit test execution in order to improve efficiency.
This behavior can be overriden by calling
setDirty
.
In JUnit tests, the values of singletons are shared across executions
of each test method.
Normally, this is a bad practice since it makes tests stateful,
potentially order-dependent, and often simply incorrect.
Yet here we have objects that must be created as singletons
in order to have stateful objects that have shared references.
Calling the
setDirty
method works around this contradiction
by forcing a refresh the SpringUnit context with the execution
of each JUnit test method.
Naturally, there is a performance penalty associated with this,
but since this is a practice to be used for integration tests,
which are assumed to be slower anyway, this may be
viewed as an acceptable consequence.
Note that the call to
setDirty
is made by the framework
(
SpringUnitTransactionalTest
) for rolled-back transactions
of the data access layer.