0

我有以下内容:

  • ExampleLogic.java(使用异步服务进行服务器调用的类)
  • ExampleServiceAsync.java(接口 á la GWT)
  • ExampleService.java(创建异步实例的接口 á la GWT)
  • ExampleLogicTest(这是出现错误的地方

我有两个简单的测试,单独运行时它们都通过了。但是当它们一个接一个地运行时(在 Eclipse 中),第二个总是失败并出现以下错误:

需要但未调用:exService.exampleServiceMethod()。实际上,与此模拟的交互为零。

我已经对服务进行了这样的注释:@GwtMock exService;
需要注意的是,调用异步服务的 ExampleLogic 类在其自己的类中创建服务。正如您在示例中看到的那样,我可以通过从测试类设置异步服务来使其工作。但后来我只需要@Mock来自 Mockito 的代替。

它有效,因此这个问题更多是出于好奇和一点点实用性(因为感觉没有必要仅为了测试而为异步服务设置设置器)。

所以问题是:
为什么会这样?
其他问题:
有什么可做的吗?你推荐另一种测试方式吗?

希望有任何 GWT 专家可以帮助我!

使用:
JUnit 4.13
GwtMockito 1.1.9(以及随后的 Mockito:0.9.2)

ExampleLogic.java(使用异步服务进行服务器调用的类)

import com.google.gwt.user.client.rpc.AsyncCallback;

public class ExampleLogic {
  public boolean callFailed; // public to simplify example
  public boolean returnVal; // public to simplify example
  private ExampleServiceAsync exampleService;
  
  public void setExampleService(ExampleServiceAsync exampleService) {
    this.exampleService = exampleService;
  }

  public void exampleCallToService() {
    if (exampleService == null) {
      exampleService = ExampleService.Util.getInstance(); // Problem arises here.
      // I suppose GwtMockito is reusing the old one even though GwtMockito.tearDown() is called.
      // That's why the second fails with the comment "There were zero interactions with this mock".
      // It is actually using the first still. Why is that so and how can I make it use the second?
    }
    
    exampleService.exampleServiceMethod(new AsyncCallback<Boolean>() {
      
      @Override
      public void onSuccess(Boolean result) {
        callFailed = false;
        returnVal = result;
      }
      
      @Override
      public void onFailure(Throwable caught) {
        callFailed = true;
      }
    });
  }
}

ExampleServiceAsync.java(接口 á la GWT)

import com.google.gwt.http.client.Request;
import com.google.gwt.user.client.rpc.AsyncCallback;

public interface ExampleServiceAsync {
  public Request exampleServiceMethod(AsyncCallback<Boolean> callback);
}

ExampleService.java(创建异步实例的接口 á la GWT)

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.RemoteService;

public interface ExampleService extends RemoteService {
  public static class Util {
    private static ExampleServiceAsync instance = null;
    public static ExampleServiceAsync getInstance(){
      if (instance == null) {
        instance = (ExampleServiceAsync) GWT.create(ExampleService.class);
      }
      
      return instance;
    }
  }
  
  boolean exampleServiceMethod();
}

ExampleLogicTest(这是出现错误的地方

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtmockito.GwtMock;
import com.google.gwtmockito.GwtMockito;
import com.google.gwtmockito.GwtMockitoTestRunner;

@RunWith(GwtMockitoTestRunner.class)
public class ExampleLogicTest {

  @GwtMock ExampleServiceAsync exService;
  // @Mock ExampleServiceAsync exService; // Can be used if the service is set manually
  @Captor ArgumentCaptor<AsyncCallback<Boolean>> callbackCaptor;
  ExampleLogic exLogic;

  @Before
  public void init() {
    GwtMockito.initMocks(this); // Doesn't make any difference to comment/uncomment.
    exLogic = new ExampleLogic();
//    exLogic.setExampleService(exService); // Uncommenting this will make both tests pass in a single run. Otherwise the second to run will always fail. Or running separately they'll pass.
  }

  @After
  public void tearDown() {
    GwtMockito.tearDown(); // Doesn't make any difference to comment/uncomment.
  }

  @Test
  public void test1_SuccessfulCall() {
    exLogic.exampleCallToService();
    Mockito.verify(exService).exampleServiceMethod(callbackCaptor.capture());
    AsyncCallback<Boolean> callback = callbackCaptor.getValue();
    callback.onSuccess(true);
    assertFalse(exLogic.callFailed);
    assertTrue(exLogic.returnVal);
  }

  @Test
  public void test2_FailedCall() {
    exLogic.exampleCallToService();
    Mockito.verify(exService).exampleServiceMethod(callbackCaptor.capture());
    AsyncCallback<Boolean> callback = callbackCaptor.getValue();
    callback.onFailure(new Throwable());
    assertTrue(exLogic.callFailed);
    assertFalse(exLogic.returnVal);
  }

}
4

1 回答 1

0
if (exampleService == null) {
   exampleService = ExampleService.Util.getInstance(); // Problem arises here.
   // I suppose GwtMockito is reusing the old one even though GwtMockito.tearDown() is called.
   // That's why the second fails with the comment "There were zero interactions with this mock".
   // It is actually using the first still. Why is that so and how can I make it use the second?
 } 
public static class Util {
  private static ExampleServiceAsync instance = null;
  public static ExampleServiceAsync getInstance(){
    if (instance == null) {
      instance = (ExampleServiceAsync) GWT.create(ExampleService.class);
    }
 
    return instance;
  }
}

你的猜测是正确的——这是你的问题。由于此 Util.instance 字段是静态的,并且在测试完成时不会将其归零,因此对 Util.getInstance() 的下一次调用必须始终返回相同的值,因此永远不会创建模拟。

需要考虑的一些可能选项:

First, creation of a service in this way is very cheap, it is possible that all of the service calls will be remade into static methods anyway, so there is nearly no actual cost to creating the service or keeping the instance around. This means you could go so far as to create a new service instance every time that the method is called, perhaps every time that any service method is called. Singletons should be used where state is important to be shared, or where the cost of creation is high - at least from the code shared here, neither of those is true.

Extending from that, you could also just directly GWT.create(...) the services as you need them, instead of a Util type that holds the instance.

Alternatively, and assuming you do want to control access to the instance (perhaps to allow for custom configuration of the service as it is created, etc), instead of a static field in a class to hold this, consider a regular field, so that a new holder can be instantiated as part of each test. If you don't want full-on DI tooling of some kind, you can still make a single instance to provide these objects. Have a test-only method that nulls out the instance during tearDown(), etc.


Quick summary of GWT options for dependency injection ("DI") tools:

  • Gin: In GWT2, "gin+guice" is quite popular, but isn't maintained any more, and will not be compatible with J2CL (another compiler which can handle much of GWT2 inputs), but is very flexible. Gin is a subset of Guice's features that can work in GWT.
  • Dagger2 is another option, but not really purpose-built to work in GWT. There are many examples that demonstrate GWT2+Dagger2, here's one that mirrors their docs https://github.com/ibaca/gwt-dagger2-coffee
  • Errai, among its other features, can act as a CDI container, but also adds many other features - almost certainly overkill for you. I would not consider it for a new project started today.
  • Crysknife is another option, specifically built to work in j2cl, but is still in progress (though artifacts are published to maven central as v0.1). This was designed as if it were only Errai's CDI features, and is much more lightweight.
于 2022-01-06T17:44:42.257 回答