Макет новых объектов в пустом конструкторе

#java #unit-testing #junit #mockito

#Ява #модульное тестирование #джунит #мокито

Вопрос:

В настоящее время я работаю над лямбдой AWS с использованием Java 11. Для реализации обработчика мне требуется пустой конструктор. Мой куратор выглядит так

 public class ApiKeyHandler {   private final SecretsManagerClient secretsManagerClient;   public ApiKeyHandler() {  secretsManagerClient = DependencyFactory.secretsManagerClient();  }   public void handleRequest(Object event, Context context) {  //Other codes here  secretsManagerClient.getSecret(/../);  } }  

И Класс Фабрики зависимостей

 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;  

но не повезло. Спасибо

Ответ №1:

Похоже, у вас есть несколько вариантов:

  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», но, по крайней мере, у вас есть код «только для тестов» только на заводе, а не во всех лямбда-функциях (я предполагаю, что у вас их много, иначе, вероятно, первый подход будет наименьшим из всех зол).

Другим возможным недостатком является то, что один и тот же экземпляр DependencyFactory (с общим статическим режимом насмешек) будет использоваться совместно между тестами, поэтому вы можете в конечном итоге «сбросить» все соответствующие насмешки после теста.

Опять же, все это осложнения, потому что в той форме, которую вы представили, нет способа внедрить зависимость в конструктор из-за ограничения технологии.

Ответ №2:

Добавьте второй конструктор, который принимает параметры:

 public ApiKeyHandler(SecretsManagerClient client) {  secretsManagerClient = client; }  public ApiKeyHandler() {  this(DependencyFactory.secretsManagerClient()); }