如何修复 java.lang.IllegalStateException:您尚未启动 Objectify 上下文

How to fix java.lang.IllegalStateException: You have not started an Objectify context

我正在尝试采用测试驱动开发方法在 App Engine 上构建基于 Java 的应用 运行,但我在设置工作时遇到困难。

我的 servlet

package mobi.grocerymonkey.groceryapp;

import com.google.appengine.api.utils.SystemProperty;

import java.io.IOException;
import java.io.BufferedReader;
import java.util.Properties;

import org.json.JSONObject;
import java.util.logging.Logger;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;

import static com.googlecode.objectify.ObjectifyService.ofy;
import com.googlecode.objectify.ObjectifyService;

/* This is the servlet */
@WebServlet(name = "GroceryServlet", value = "/grocery")
public class GroceryServlet extends HttpServlet {

  private static final Logger log = Logger.getLogger(GroceryServlet.class.getName());

  @Override
  public void init() throws ServletException {
    log.info("context init");
    ObjectifyService.init();
    ObjectifyService.register(Grocery.class);
  }

  @Override
  public void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {

    response.setContentType("text/plain");
    response.getWriter().println("Hello Kitty");
  }

  @Override
  public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException {
      BufferedReader reader = request.getReader();
      String line = null;
      StringBuffer stringBuffer = new StringBuffer();
      while((line = reader.readLine()) != null) {
        stringBuffer.append(line);
      }
      String jsonString = stringBuffer.toString();
      JSONObject json = new JSONObject(jsonString);
      log.info("JSON "+ jsonString);

      Grocery grocery = new Grocery();
      grocery.setName((String) json.get("name"));
      grocery.setQuantity((Integer) json.get("quantity"));

      ofy().save().entity(grocery).now();

      log.info("JSON name "+ grocery.getName());

      response.setContentType("application/json");
      response.getWriter().println(jsonString);
    }

}

web.xml 文件

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>
  <filter>
    <filter-name>ObjectifyFilter</filter-name>
    <filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>ObjectifyFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>FORWARD</dispatcher>
  </filter-mapping>
</web-app>

我的单元测试

package mobi.grocerymonkey.groceryapp;

import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;

import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.cloud.datastore.DatastoreOptions;

import com.google.cloud.datastore.DatastoreOptions;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.ObjectifyService;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.json.JSONObject;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.BufferedReader;
import java.io.StringReader;
import java.io.Reader;
import java.io.Closeable;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Unit tests for {@link HelloAppEngine}.
 */
@RunWith(JUnit4.class)
public class GroceryServletTest {
  private static final String MOCK_URL = "/grocery";
  // Set up a helper so that the ApiProxy returns a valid environment for local testing.
  private final LocalServiceTestHelper helper =
    new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
  private Closeable closeable;

  @Mock
  private HttpServletRequest mockRequest;

  @Mock
  private HttpServletResponse mockResponse;

  private StringWriter responseWriter;
  private GroceryServlet servletUnderTest;

  @Before
  public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
    helper.setUp();

    ObjectifyService.init(new ObjectifyFactory(
      DatastoreOptions.newBuilder()
        .setHost("http://localhost:8081")
        .setProjectId("enduring-trees-259812")
        .build()
        .getService()
      ));
      closeable = ObjectifyService.begin();

    //  Set up some fake HTTP requests
    when(mockRequest.getRequestURI()).thenReturn(MOCK_URL);

    JSONObject grocery = new JSONObject();
    grocery.put("name", "Beer");

    Reader inputString = new StringReader(grocery.toString());
    BufferedReader reader = new BufferedReader(inputString);
    when(mockRequest.getReader()).thenReturn(reader);

    // Set up a fake HTTP response.
    responseWriter = new StringWriter();
    when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));

    servletUnderTest = new GroceryServlet();
    servletUnderTest.init();
  }

  @After public void tearDown() throws Exception {
    closeable.close();
    helper.tearDown();
  }

  @Test
  public void doGetWritesResponse() throws Exception {
    servletUnderTest.doGet(mockRequest, mockResponse);

    // We expect our hello world response.
    assertThat(responseWriter.toString())
        .contains("Hello Kitty");
  }

  @Test
  public void doPostWritesResponse() throws Exception {
    JSONObject reqObj = new JSONObject();
    reqObj.put("name", "Beer");
    reqObj.put("quantity", 5);
    StringReader reader = new StringReader(reqObj.toString());

    when(mockRequest.getReader()).thenReturn(new BufferedReader(new StringReader(reqObj.toString())));

    servletUnderTest.doPost(mockRequest, mockResponse);

    // We expect our hello world response.
    assertThat(responseWriter.toString())
        .contains(reqObj.getString("name"));
  }
}

测试失败并显示以下错误消息

[ERROR] Tests run: 2, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.103 s <<< FAILURE! - in mobi.grocerymonkey.groceryapp.GroceryServletTest [ERROR] doPostWritesResponse(mobi.grocerymonkey.groceryapp.GroceryServletTest) Time elapsed: 0.078 s <<< ERROR! java.lang.IllegalStateException: You have not started an Objectify context. You are probably missing the ObjectifyFilter. If you are not running in the context of an http request, see the ObjectifyService.run() method. at mobi.grocerymonkey.groceryapp.GroceryServletTest.doPostWritesResponse(GroceryServletTest.java:109)

这是由我的 servlet 中的这一行 y().save().entity(grocery).now() 引起的。当我删除它时,测试 运行 没有错误。

我曾尝试采用不同的方法来解决在 Whosebug 上发现的此错误,但没有成功。

应该如何设置 test/app 才能使用测试驱动方法进行开发?我正在寻找的是一种能够先编写单元测试然后再编写实际应用程序的方法。但是如何成功呢?

(免责声明,我已经有十多年没有使用 Java 了)

更新

ServletContext 文件

package mobi.grocerymonkey.groceryapp;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

import java.io.Closeable;
import java.io.IOException;

import com.googlecode.objectify.ObjectifyService;
import static com.googlecode.objectify.ObjectifyService.ofy;

@WebListener
public class GroceryContextListener implements ServletContextListener {

    private ServletContext context;
    private Closeable closeable;

    public void contextInitialized(ServletContextEvent event) {
        this.context = event.getServletContext();

        ObjectifyService.init();
        this.closeable = ObjectifyService.begin();
        ObjectifyService.register(Grocery.class);
        System.out.println("Context initialized");
    }

    public void contextDestroyed(ServletContextEvent event) {
      try {
        this.closeable.close();
      } catch(IOException ioe) {

      }
    }
}

单元测试文件

package mobi.grocerymonkey.groceryapp;

import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;

import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.cloud.datastore.DatastoreOptions;

import com.google.cloud.datastore.DatastoreOptions;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.ObjectifyService;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.json.JSONObject;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.BufferedReader;
import java.io.StringReader;
import java.io.Reader;
import java.io.Closeable;
import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletContextEvent;

/**
 * Unit tests for {@link HelloAppEngine}.
 */
@RunWith(JUnit4.class)
public class GroceryServletTest {
  private static final String MOCK_URL = "/grocery";
  // Set up a helper so that the ApiProxy returns a valid environment for local testing.
  private final LocalServiceTestHelper helper =
    new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
  private Closeable closeable;

  @Mock
  private HttpServletRequest mockRequest;

  @Mock
  private HttpServletResponse mockResponse;

  private ServletContextListener contextListener;
  private ServletContext context;

  private StringWriter responseWriter;
  private GroceryServlet servletUnderTest;

  @Before
  public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
    helper.setUp();

    contextListener = new GroceryContextListener();
    context = mock(ServletContext.class);

    //  Set up some fake HTTP requests
    when(mockRequest.getRequestURI()).thenReturn(MOCK_URL);

    JSONObject grocery = new JSONObject();
    grocery.put("name", "Beer");

    Reader inputString = new StringReader(grocery.toString());
    BufferedReader reader = new BufferedReader(inputString);
    when(mockRequest.getReader()).thenReturn(reader);

    // when(mockRequest.getServletContext()).thenReturn(context);

    // Set up a fake HTTP response.
    responseWriter = new StringWriter();
    when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));

    servletUnderTest = new GroceryServlet();
  }

  @After 
  public void tearDown() throws Exception {
    helper.tearDown();
  }

  @Test
  public void doGetWritesResponse() throws Exception {
    servletUnderTest.doGet(mockRequest, mockResponse);

    // We expect our hello world response.
    assertThat(responseWriter.toString())
        .contains("Hello Kitty");
  }

  @Test
  public void doPostWritesResponse() throws Exception {
    contextListener.contextInitialized(new ServletContextEvent(context));

    JSONObject reqObj = new JSONObject();
    reqObj.put("name", "Beer");
    reqObj.put("quantity", 5);
    StringReader reader = new StringReader(reqObj.toString());

    when(mockRequest.getReader()).thenReturn(new BufferedReader(new StringReader(reqObj.toString())));

    servletUnderTest.doPost(mockRequest, mockResponse);

    // We expect our hello world response.
    assertThat(responseWriter.toString())
        .contains(reqObj.getString("name"));
  }
}

Servlet 文件

package mobi.grocerymonkey.groceryapp;

import com.google.appengine.api.utils.SystemProperty;

import java.io.IOException;
import java.io.BufferedReader;
import java.util.Properties;

import org.json.JSONObject;
import java.util.logging.Logger;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;

import static com.googlecode.objectify.ObjectifyService.ofy;
import com.googlecode.objectify.ObjectifyService;

/* This is the servlet */
@WebServlet(name = "GroceryServlet", value = "/grocery")
public class GroceryServlet extends HttpServlet {

  private static final Logger log = Logger.getLogger(GroceryServlet.class.getName());

  @Override
  public void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {

    response.setContentType("text/plain");
    response.getWriter().println("Hello Kitty");
  }

  @Override
  public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException {
      BufferedReader reader = request.getReader();
      String line = null;
      StringBuffer stringBuffer = new StringBuffer();
      while((line = reader.readLine()) != null) {
        stringBuffer.append(line);
      }
      String jsonString = stringBuffer.toString();
      JSONObject json = new JSONObject(jsonString);
      log.info("JSON "+ jsonString);

      Grocery grocery = new Grocery();
      grocery.setName((String) json.get("name"));
      grocery.setQuantity((Integer) json.get("quantity"));

      ofy().save().entity(grocery).now();

      log.info("JSON name "+ grocery.getName());

      response.setContentType("application/json");
      response.getWriter().println(jsonString);
    }

}

现在我在 运行 测试时遇到 "com.google.cloud.datastore.DatastoreException: Unauthenticated" 错误,看起来我的方法是正确的。我是否会将数据存储凭据存储在 web.xml 中,然后将它们传递给类似于

的上下文
ObjectifyService.init(new ObjectifyFactory(
      DatastoreOptions.newBuilder()
        .setHost("http://localhost:8081")
        .setProjectId("enduring-trees-259812")
        .build()
        .getService()
      ));
    ObjectifyService.factory().register(Grocery.class);

新更新

我升级到 Junit5 并将整个测试重写为这个

package mobi.grocerymonkey.groceryapp;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.util.Closeable;
import static com.googlecode.objectify.ObjectifyService.factory;
import static com.googlecode.objectify.ObjectifyService.ofy;
import com.googlecode.objectify.Key;

import com.google.cloud.datastore.Datastore;
import com.google.cloud.datastore.DatastoreOptions;
import com.google.cloud.datastore.testing.LocalDatastoreHelper;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;

import mobi.grocerymonkey.groceryapp.util.TestBase;

import mobi.grocerymonkey.groceryapp.domain.Grocery;
import mobi.grocerymonkey.groceryapp.domain.GroceryList;

public class MyFirstTest extends TestBase {

  // Maximum eventual consistency.
  private final static LocalServiceTestHelper helper =
      new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()
          .setDefaultHighRepJobPolicyUnappliedJobPercentage(100));

  Closeable closeable;

  @BeforeAll
  public static void setUp() {
    helper.setUp();
  }

  @AfterAll
  public static void tearDown() {
    helper.tearDown();
  }

  @BeforeEach
  public void setUpEach() {
    ObjectifyService.init(new ObjectifyFactory(
      DatastoreOptions.getDefaultInstance().getService()));
    closeable = ObjectifyService.begin();
  }

  @AfterEach
  public void tearDownEach() {
    closeable.close();
  }

  @DisplayName("Test MyFirstTest.testAddition()")
  @Test
  public void testAddition() {
    assertEquals(1 + 1, 2);
  }

  @DisplayName("Testing testGroceryList()")
  @Test
  public void testGroceryList() {
    factory().register(GroceryList.class);

    GroceryList list = new GroceryList("Weekend Beer List");
    Key<GroceryList> k1 = ofy().save().entity(list).now();

    assertEquals(1+1, 2);
  }
}

它暂时被特意保存在一个文件中。但由于某种原因,当测试为 运行 时,数据存储无法找到 运行ning 的模拟器。我收到 Datastore Unauthenticated 错误。

我 运行 gcloud beta emulators datastore start$(gcloud beta emulators datastore env-init) 在 运行 进行单元测试之前。

测试驱动开发的核心围绕五个步骤展开,您在整个软件开发生命周期中不断重复这些步骤。

测试驱动开发生命周期:

  1. 写测试

  2. 运行测试(没有实现代码,测试不通过)

  3. 编写足够的实现,使测试通过

  4. 运行所有测试(测试通过)

  5. 重构

  6. 重复

按照这些步骤,您可以为您的应用程序创建 TDD 实现。

除了我上面指定的步骤之外,Google Cloud 没有具体的方法。

如错误中所述,您可以看到尚未启动 Objectify 上下文,并且缺少 ObjectifyFilter。

这是 Java 中列表的 implementation,它遵循 TDD,可能有助于消除您的一些顾虑。

问题是你调用了 ObjectifyService.init() 两次,但你只调用了第一个(废弃的)工厂的 begin()

您在 setUp() 方法中调用 init(),该方法初始化静态 ObjectifyFactory。然后,您使用 ObjectifyService.begin() 调用在该工厂上打开一个会话。

setUp() 的末尾调用 servletUnderTest.init(),它也调用 ObjectifyService.init()。这取代了静态 ObjectifyFactory。当您下次执行 servlet 并调用 ofy()... 时,您使用的是尚未启动会话的工厂。

查看 ObjectifyService 的代码。从字面上看,它只是几行代码来包装一个静态 ObjectifyFactory 实例。

如果您有多个 servlet,此代码在生产中也无法正常工作 - 您只想初始化并注册您的 类 一次。我建议使用 ServletContextListener.