Sunday, November 1, 2009

Unit Testing with App Engine and Spring

Sculptor makes it easy to write JUnit tests for Google App Engine. A test case looks like this:


public class SupplierServiceTest extends AbstractAppEngineJpaTests {

@Autowired
private SupplierService supplierService;

@Before
public void populateDatastore() {
Supplier supplier1 = new Supplier("S1");
getEntityManager().persist(supplier1);

Supplier supplier2 = new Supplier("S2");
getEntityManager().persist(supplier2);
}

@Test
public void testFindAll() throws Exception {
List<Supplier> all = supplierService.findAll(getServiceContext());
assertEquals(2, all.size());
}

@Test
public void testFindByName() throws Exception {
Supplier found = supplierService.findByName(getServiceContext(), "S2");
assertNotNull(found);
assertEquals("S2", found.getName());
}
}


Very natural!

It is interesting to take a look at the base class. It defines a few annotations and extends AbstractJUnit4SpringContextTests to initialize the Spring environment. This enables usage of ordinary @Autowire dependency injection directly in the test class.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext-test.xml"})
public abstract class AbstractAppEngineJpaTests
extends AbstractJUnit4SpringContextTests {

The embedded App Engine environment is initialized from a method annotated with @Before, i.e. invoked before each test method.

public static void setUpAppEngine(ApiProxy.Environment testEnvironment) {
ApiProxy.setEnvironmentForCurrentThread(testEnvironment);

ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")) {
});

ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
proxy.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY, Boolean.TRUE.toString());
clearSentEmailMessages();
}

public static void tearDownAppEngine() {
ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
LocalDatastoreService datastoreService = (LocalDatastoreService) proxy.getService("datastore_v3");
datastoreService.clearProfiles();
clearSentEmailMessages();
}

It is initialized with in memory data store, i.e. it is empty before each test method. You may populate it with initial data in your subclass in a @Before method, see populateDataStore in the sample above.

I learned one thing when doing junit testing in the app engine environment. When working with ordinary databases I have found the Spring transactional test support useful, i.e. Spring executes each test method in a transaction, which is rolled back after the test mehtod. That is achieved with the following annotations and usage of the annotation @BeforeTransaction instead of the ordinary @Before.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext-test.xml"})
@TestExecutionListeners(TransactionalTestExecutionListener.class)
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = true)
@Transactional
public abstract class AbstractAppEngineJpaTests
extends AbstractJUnit4SpringContextTests {

That was my initial approach also with app engine, but I realized that it was not good. Look at the following test. It will fail on the last assert when using the above transactional support.


@Test
public void testSave() throws Exception {
int countBefore = countRowsInTable(Supplier.class);
Supplier supplier3 = new Supplier("S3");
supplierService.save(getServiceContext(), supplier3);
int countAfter = countRowsInTable(Supplier.class);
assertEquals(countBefore + 1, countAfter);
}

The reason is that queries see a snapshot of the datastore as of the beginning of the transaction.

Data isolation between test methods is no problem, since the datastore is initialized (empty) before each test method.

That's all! Try it yourself by running the Maven Archetype for App Engine and fill in the details in the generated PlanetServiceTest.
  1. mvn archetype:generate -DarchetypeGroupId=org.fornax.cartridges -DarchetypeArtifactId=fornax-cartridges-sculptor-archetype-appengine -DarchetypeVersion=1.7.0-SNAPSHOT -DarchetypeRepository=http://www.fornax-platform.org/archiva/repository/snapshots/
  2. mvn clean eclipse:eclipse

Stay tuned, in next post I will describe how to mock.

4 comments:

  1. My current solution is for my DAOs to use transactions. The service layer will also use transactions and the DAOs will "inherit" the existing service layer transactions.

    The DAOs use transactions for the unit/integration tests, so that Spring can do its job of opening and closing the persistence manager.

    http://xrl.in/3j6y

    It's very much a work in progress and changing daily.

    ReplyDelete
  2. Running tests via Maven works but running as JUnit tests from within Eclipse fails with 'java.lang.IllegalStateException: Failed to load ApplicationContext'. Would you know why?

    Similar problem was reported here:
    http://old.nabble.com/Question-on-error-running-unit-tests-in-eclipse-td25417429s17564.html

    ReplyDelete
  3. I got past the problem I reported above. M2Eclipse does not produce the .project and .classpath files correctly. 'mvn eclipse:eclipse' creates the eclipse project files such that the JUnit tests run without problems.

    ReplyDelete