TurboManage

David Chandler's Journal of Java Web and Mobile Development

  • David M. Chandler


    Web app developer since 1994 and Google Cloud Platform Instructor now residing in Colorado. Besides tech, I enjoy landscape photography and share my work at ColoradoPhoto.gallery.

  • Subscribe

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 223 other followers

  • Sleepless Nights…

    March 2010
    S M T W T F S
     123456
    78910111213
    14151617181920
    21222324252627
    28293031  
  • Blog Stats

    • 1,029,260 hits

Archive for March 3rd, 2010

A recipe for unit testing AppEngine task queues

Posted by David Chandler on March 3, 2010

One of the difficulties of testing AppEngine code has been the problem of running unit tests for code written against the Task Queue API. AppEngine provided no easy way to programatically control the task queue engine. In addition, tasks are servlets, so it’s not particularly easy to invoke them with the correct task payload in a unit test.

Fortunately, AppEngine 1.3.1 provides test helpers that greatly simplify writing unit tests against the Datastore and Task Queue APIs (as well as Memcache, Blobstore, and others). By default, the Datastore is configured with the NO_STORAGE property set to true (the default) so that each test method starts with a blank datastore. Also by default, the task queuing auto-run capability is disabled, which gives you the opportunity to programatically run a task once you have verified that it’s been enqueued properly.

The following BaseTest class initializes the Datastore and TaskQueue services. Once the AppEngine team fixes the bug that requires the LocalServerEnvironment override, it will be very simple, indeed. You can ignore the Guice injector stuff if you’re not using Guice.

BaseTest.java

package com.roa.test.helper;

import java.io.File;

import junit.framework.TestCase;

import com.google.appengine.tools.development.LocalServerEnvironment;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.roa.server.guice.ServerModule;
import com.turbomanage.gwt.server.guice.DispatchTestModule;

/**
 * A simple test base class that can be extended to build unit tests that
 * properly construct and tear down AppEngine test environments.
 */
public abstract class BaseTest extends TestCase
{

	private static Injector inj;
	protected LocalServiceTestHelper helper;
	private LocalDatastoreServiceTestConfig datastoreConfig;
	private LocalTaskQueueTestConfig taskQueueConfig;

	/**
	 * Sets up the AppEngine environment and initializes Guice.
	 */
	@Override
	protected void setUp() throws Exception
	{
		super.setUp();
		datastoreConfig = new LocalDatastoreServiceTestConfig();
		taskQueueConfig = new LocalTaskQueueTestConfig();
		helper = new LocalServiceTestHelper(datastoreConfig, taskQueueConfig)
		{
			// Temp workaround until 1.3.2 to help task queue find war/WEB-INF/queue.xml
			@Override
			protected LocalServerEnvironment newLocalServerEnvironment()
			{
				final LocalServerEnvironment lse = super.newLocalServerEnvironment();
				return new LocalServerEnvironment()
				{
					public File getAppDir()
					{
						return new File("war");
					}

					public String getAddress()
					{
						return lse.getAddress();
					}

					public int getPort()
					{
						return lse.getPort();
					}

					public void waitForServerToStart() throws InterruptedException
					{
						lse.waitForServerToStart();
					}
				};
			}
		};

		helper.setEnvAuthDomain("auth");
		helper.setEnvEmail("test@example.com");
		helper.setEnvIsAdmin(true);
		helper.setEnvIsLoggedIn(true);
		helper.setUp();

		inj = Guice.createInjector(new ServerModule(), new DispatchTestModule());
	}

	@Override
	protected void runTest() throws Throwable
	{
		super.runTest();
	}

	/**
	 * Deconstructs the AppEngine environment.
	 */
	@Override
	protected void tearDown() throws Exception
	{
		helper.tearDown();
		super.tearDown();
	}

	/**
	 * Provide Guice injector to tests
	 */
	protected static Injector getInj()
	{
		return inj;
	}

}

Code under test will now have access to the Datastore and the TaskQueue API just as in dev and production environments.

The new task queue helper (LocalTaskQueueTestConfig) provides methods to inspect the task queue and verify task creation. When you call runTask(), the task queue test service will attempt to invoke the servlet associated with your task; however, unless you can figure out a way to run your unit test in a servlet container, your task servlet won’t actually be reachable by the runTask() call (at least, I’m assuming this is why I’m seeing “connection refused” errors).

Fortunately, we can easily simulate what the AppEngine task queue does by invoking the task servlet using ServletUnit. The test case below creates a simple test case using Vince Bonfanti’s Deferred task servlet, verifies that it has been enqueued properly, and then invokes it using ServletUnit.

package com.roa.test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;

import javax.servlet.ServletException;

import org.xml.sax.SAXException;

import com.google.appengine.api.labs.taskqueue.dev.LocalTaskQueue;
import com.google.appengine.api.labs.taskqueue.dev.QueueStateInfo;
import com.google.appengine.api.labs.taskqueue.dev.QueueStateInfo.TaskStateInfo;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
import com.meterware.httpunit.PostMethodWebRequest;
import com.meterware.servletunit.ServletRunner;
import com.meterware.servletunit.ServletUnitClient;
import com.newatlanta.appengine.taskqueue.Deferred;
import com.newatlanta.appengine.taskqueue.Deferred.Deferrable;
import com.roa.test.helper.BaseTest;

public class SimpleTaskTest extends BaseTest implements Serializable
{

	@Override
	protected void setUp() throws Exception
	{
		super.setUp();
		// Do additional setup here
	}

	public class HelloUserTask implements Deferrable
	{
		private User user;

		public HelloUserTask(User u)
		{
			this.user = u;
		}

		@Override
		public void doTask() throws ServletException, IOException
		{
			System.out.println("Hello, " + user.getNickname());
		}
	}

	public void testTaskInvocation() throws IOException, SAXException
	{
		User testUser = UserServiceFactory.getUserService().getCurrentUser();
		HelloUserTask task = new HelloUserTask(testUser);
		// Queue the task
		Deferred.defer(task);

		// Verify that one task has been enqueued
		LocalTaskQueue taskQueue = LocalTaskQueueTestConfig.getLocalTaskQueue();
		Map<String, QueueStateInfo> allQueues = taskQueue.getQueueStateInfo();
		QueueStateInfo deferredQueue = allQueues.get("deferred");
		assertEquals(1, deferredQueue.getCountTasks());
		TaskStateInfo taskInfo = deferredQueue.getTaskInfo().get(0);
		String queuedTask = taskInfo.getBody();
		String taskName = taskInfo.getTaskName();

		// Run task will fail because no servlet container running
		// taskQueue.runTask("deferred", taskName);

		// Run using ServletUnit instead
		ServletRunner sr = new ServletRunner();
		sr.registerServlet("/_ah/queue/deferred", "com.newatlanta.appengine.taskqueue.Deferred");
		ServletUnitClient client = sr.newClient();
		client.sendRequest(new PostMethodWebRequest("http:/_ah/queue/deferred",
			new ByteArrayInputStream(queuedTask.getBytes()), null));

		// Flush task queue (removes all tasks)
		taskQueue.flushQueue("deferred");
	}

}

When you run this, you’ll see “Hello, test@example.com” written to the console by the task.

Using the AppEngine test helpers and ServletUnit, we now have an easy way to verify that tasks have been queued as expected AND a way to invoke the task servlets in the same test case.

Happy testing!

Posted in AppEngine | 13 Comments »

 
%d bloggers like this: