在空构造函数中模拟新对象

Mock new objects in empty constructor

我目前正在使用 Java11 开发 AWS Lambda。它要求我实现处理程序以具有空构造函数。我的处理程序看起来像这样

public class ApiKeyHandler {

  private final SecretsManagerClient secretsManagerClient;

  public ApiKeyHandler() {
    secretsManagerClient = DependencyFactory.secretsManagerClient();
  }

  public void handleRequest(Object event, Context context) {
    //Other codes here
    secretsManagerClient.getSecret(/../);
  }
}

和依赖工厂class

public class DependencyFactory {

    private DependencyFactory() {}

    /**
     * @return an instance of SecretsManagerClient
     */
    public static SecretsManagerClient secretsManagerClient() {
        return SecretsManagerClient.builder()
            .region(/**/)
            .build();
    }


}

现在,当我尝试为此编写单元测试时,我无法在构造函数中模拟对象。有什么办法可以模拟它吗?

我试过了

@Mock SecretsManagerClient secretsManagerClient;
@InjectMocks ApiKeyHandler handler;

但运气不好。谢谢

添加第二个接受参数的构造函数:

public ApiKeyHandler(SecretsManagerClient client) {
  secretsManagerClient = client;
}

public ApiKeyHandler() {
  this(DependencyFactory.secretsManagerClient());
}

看起来你有几个选择:

  1. 您可以添加另一个带有要注入参数的构造函数。从测试的角度来看,这既简单又干净,但毕竟您将拥有仅用于测试的生产代码(在本例中为此构造函数)。 一般来说我不提倡这种方法,虽然我知道这里有技术限制。
  2. 您可以模拟依赖工厂。由于调用是静态的,您最终可能会使用实际上可以模拟静态调用的 PowerMock / PowerMockito。这是一种维护起来非常痛苦的东西,一般来说,现在不鼓励这种方法。
  3. 您可以重写 DependencyFactory,以便可以使用某种模拟实现配置它(这将允许指定模拟依赖项):
public interface DependencyFactoryMode {
   SecretsManagerClient secretsManagerClient();  
}

public class RealDependencyFactoryMode implements DependencyFactoryMode {
    public SecretsManagerClient secretsManagerClient() {
        return SecretsManagerClient.builder()
            .region(/**/)
            .build();
  
    }
}
// in src/test/java - test code in short
public class DependencyFactoryTestMode implements DependencyFactoryMode {
    private SecretsManagerClient smc = Mockito.mock(SecretsManagerClient.class);
    public SecretsManagerClient secretsManagerClient() {
       return smc;
    }
    // this will be used in tests
    public SecretsManagerClient getSmcMock() {return smc;}
}

public class DependencyFactory {
   private static DependencyFactoryMode mode;

   static {
      // depending on the configuration, external properties or whatever 
      // initialize in production mode or test mode
      // of course this is the most "primitive" implementation you can probably
      // do better
      if(isTest) {
         mode = new TestDependencyFactoryTestMode();
      } else {
         // this is a default behavior
         mode = new RealDependencyFactoryMode();
      }
   }

   private DependencyFactory() {}
   public static DependencyFactoryMode getMode() {
      return mode;
   }
   public static SecretsManagerClient secretsManagerClient() {
      return mode.secretsManagerClient();
   }

}

使用这种方法,您必须预先配置依赖项工厂,以便在 运行 测试时它会“知道”它应该 运行 在测试模式下。

public class Test {
    @Test
    public void test() {
      // DependencyFactoryMode will be used in the test mode
      DependecyFactoryMode testMode = DependencyFactory.getMode(); 
      var smc = testMode.secretsManagerClient();
      Mockito.when(smc.foo()).thenReturn(...);
    }
} 

现在这种方法有与“1”相同的缺点,但至少你在工厂中有一个“仅用于测试”的代码,而不是在所有 lambda 函数中(我假设你有很多,否则可能第一种方法是所有弊端中最少的。

另一个可能的缺点是 DependencyFactory 的相同实例(使用共享静态模拟模式)将在测试之间共享,因此您可能会在测试后“重置”所有相关模拟。

同样,这些都是复杂的,因为在您提供的形式中,由于技术限制,无法在构造函数中提供依赖项注入。