Integration tests for Google App Engine (java)

2019-03-19 06:28发布

问题:

I'm trying to develop some effective integration tests for my GAE/j application. I'm familiar with https://developers.google.com/appengine/docs/java/tools/localunittesting -- these tools are great for small unit tests. I'm now interested in developing integration tests that test actual web requests. For example, I'd like to test that web.xml is mapping servlets and filters to the expected URLs and test that my JSPs generate what I expect.

My aim was to bring up a local development server inside the JVM, which I could fire requests against. I'm open to other integration strategies, though; as I say above, I just want to effectively test JSP generation and other request-level features.

I've managed to use DevAppServerFactory to start a development server in the same JVM. However, it appears that the DevAppServer this generates uses a separate classloader from the main JVM. This makes testing a lot more challenging--I can't use any of the local unittesting Local*TestConfig classes to control behavior of this server. Similarly, I can't "roll my own" hooks for modifying behavior via e.g. statics, since the statics I can mutate in the test harness aren't the same statics the DevAppServer is looking at. This makes it challenging to skip features not central to the current test (e.g. requiring login), to inject failures, to inject mocks, etc. This really limits how completely and efficiently I can test my code.

I've found a real dearth of documentation on the web for integration testing with App Engine. I'm sure someone has done this before...are there any tips or resources out there you can share?

回答1:

Basically, you need to do two things:

  1. Add two servlets (or whatever), which must be only enabled during testing, which allow you to invoke setup and teardown on the helper remotely.
  2. Make your servlet engine serve requests in a completely single-threaded way. This is needed because for some reason, the Helper class Google provides only takes effect in the current thread.


回答2:

I agree this issue is poorly documented.
I managed to write an end-to-end test that starts a server as a black box and sends it HTTP requests.

It works like this:

package com.project.org;

import static org.junit.Assert.assertEquals;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.appengine.tools.development.testing.BaseDevAppServerTestConfig;
import com.google.appengine.tools.development.testing.DevAppServerTest;
import com.google.appengine.tools.development.testing.DevAppServerTestRunner;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;

@RunWith(DevAppServerTestRunner.class)
@DevAppServerTest(HelloWorldTest.TestConfig.class)
public class HelloWorldTest {

  public class TestConfig extends BaseDevAppServerTestConfig {

    @Override public File getSdkRoot() {
      // You may need to tweak this.
      return new File("../../appengine-java-sdk-1.9.15");
    }

    @Override public File getAppDir() {
      return new File("war");
    }

    @Override
    public List<URL> getClasspath() {
      // There may be an easier way to do this.
      List<URL> classPath = new ArrayList<>();
      try {
        String separator = System.getProperty("path.separator");
        String[] pathElements = System.getProperty("java.class.path").split(separator);
        for (String pathElement : pathElements) {
          classPath.add(new File(pathElement).toURI().toURL());
        }
      }
      catch (MalformedURLException e) {
        throw new RuntimeException(e);
      }
      return classPath;
    }
  }

  private final LocalServiceTestHelper testHelper;
  private final String port;

  public HelloWorldTest() {
    testHelper = new LocalServiceTestHelper();
    port = System.getProperty("appengine.devappserver.test.port");
  }

  @Before public void setUpServer() {
    testHelper.setUp();
  }

  @After public void tearDown() {
    testHelper.tearDown();
  }

  @Test public void testHelloWorld() throws Exception {
    URL url = new URL("http://localhost:" + port + "/hello");
    HTTPResponse response = URLFetchServiceFactory.getURLFetchService().fetch(url);
    assertEquals(200, response.getResponseCode());
    assertEquals("Hello world!", new String(response.getContent(), "UTF-8"));
  }
}

Now the issue I have is that if you have two of those tests, with each passing individually, you cannot run them in the same binary. The exception on line 37 of this file get thrown:

IllegalStateException("Dev Appserver is already running.")

Not sure how to fix this.