GwtMockito:在测试 class 中使用异步服务时,单个测试通过,多个失败。 (GwtMock 和 GWT.create 相关)

GwtMockito: Single tests pass and multiple fails when using Async-service within tested class. (GwtMock- and GWT.create-related)

我有以下内容:

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

Wanted but not invoked: exService.exampleServiceMethod(). Actually there were zero interactions with this mock.

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

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

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

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

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

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

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(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(用于创建异步实例的 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);
  }

}
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() 必须始终 return 相同的值,因此模拟永远不会已创建。

一些可能的考虑选项:

首先,以这种方式创建服务的成本非常低,有可能所有的服务调用都将被重新制作成静态方法,因此创建服务或保留实例几乎没有实际成本大约。这意味着您甚至可以在每次调用该方法时创建一个新的服务实例,也许每次调用 any 服务方法时。单例应该用于状态共享很重要的地方,或者创建成本很高的地方 - 至少从这里共享的代码来看,这些都不是真的。

由此扩展,您还可以根据需要直接 GWT.create(...) 服务,而不是保存实例的 Util 类型。

或者,假设您确实想要控制对实例的访问(可能允许在创建服务时自定义配置等),而不是 class 中的静态字段来保存为此,请考虑一个常规字段,以便可以将新的持有者实例化为每个测试的一部分。如果您不想要某种完整的 DI 工具,您仍然可以创建一个实例来提供这些对象。有一个只测试方法,在 tearDown() 等期间清空实例


依赖注入(“DI”)工具的 GWT 选项快速总结:

  • Gin: In GWT2, "gin+guice" 非常流行,但不再维护,并且不会与 J2CL(另一个可以处理大部分 GWT2 输入的编译器)兼容,但非常灵活。Gin 是一个可以在 GWT 中工作的 Guice 功能的子集。
  • 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 除了它的其他功能外,还可以充当 CDI 容器,而且还添加了许多其他功能 - 几乎肯定对您来说太过分了。对于今天开始的新项目,我不会考虑它。
  • Crysknife 是另一种选择,专门为在 j2cl 中工作而构建,但仍在进行中(尽管工件作为 v0.1 发布到 Maven Central)。这设计得好像它只是 Errai 的 CDI 功能,而且更轻量级。