Пружина, вводящая инициализированный макет через конструктор

#java #spring #junit #dependency-injection #mockito

Вопрос:

У меня есть одноэлементный класс (так называемый частный конструктор), который должен использовать хранилище данных Spring во время инициализации. У меня есть один, введенный в качестве аргумента конструктора. Грубо:

 @Controller
public class MyClass {
    @Autowired
    private MyClass(MyRepository repo) {
        repo.findAll();
    }
}
 

Я хочу модульно протестировать свой класс, поэтому мне нужно, чтобы макет репозитория был инициализирован с помощью макетных значений, а затем передан в мой класс до инициализации моего класса. Как мне написать свои насмешки над Mockito в тесте JUnit, чтобы это стало возможным?

Ответ №1:

Вам не нужна пружина; это преимущество инъекции конструктора. Просто используйте MyRepository mockRepo = mock(MyRepository.class) и new MyClass(mockRepo) .

(Кстати, ваш конструктор должен быть общедоступным. Похоже, вы совершаете распространенную ошибку, путаете разные значения слова «синглтон»; в случае DI это просто означает, что контейнер создает только один экземпляр и разделяет его. Наконец, если у вас есть только один конструктор, вам он не нужен @Autowired .)

Комментарии:

1. При использовании с реальными данными мой класс займет немного времени для инициализации и будет использовать нетривиальный объем памяти, поэтому я хочу явно гарантировать, что когда-либо будет только один экземпляр моего класса.

2. @KonstantinTarashchanskiy Это существенный запах кода, и тем более причина для очистки вашей структуры; например, в тесте вы должны иметь возможность возвращать несколько сгенерированных элементов (а также ноль элементов!), findAll чтобы всесторонне протестировать поведение.

3. Я согласен с Крилис-осторожно оптимистично — сделать конструктор в классе общедоступным и установить область действия компонента весной как одноэлементную. Это сделало бы это тривиальным для тестирования и по-прежнему поддерживало бы один экземпляр в контексте Spring, что довольно часто означает один экземпляр на JVM. Если по какой-либо причине вы абсолютно не можете этого сделать, полностью удалите модификатор доступа (используйте доступ по умолчанию / пакет) и аннотируйте конструктор с помощью @VisibleForTesting. Ваш тест по-прежнему остается тривиальным, и никакие классы за пределами пакета, содержащего контроллер, не смогут вызвать конструктор.

Ответ №2:

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

Предполагая, что вы используете JUni5 и имеете findAllStudents() в своем контроллере. Итак, ваш тестовый файл примерно такой

 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MyClassTest {

    @Mock
    private MyRepository repo;
    
    @InjectMocks
    private MyClass controller;

    @BeforeAll
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testDataIsExist() {
        List<String> expectednames = new ArrayList();
        expectedNames.add("Foo");
        expectedNames.add("Bar");

        Mockito.when(myRepo.findAll()).thenReturn(expectedNames);

        List<String> result = controller.findAllStudents();
        Assertions.assertNotNull(result);
        Assertions.assertEquals(expectednames, result);
    }
}
 

Таким образом, мы издеваемся над всеми службами, которые мы используем в контроллере, а затем вводим их в сам контроллер. Затем в методе тестирования мы имитируем repo.findAll() возврат expectedNames , поэтому, если контроллер найдет эту функцию, он вернет то, что говорит вернуть мок.

После вызова функции мы должны убедиться, что результат соответствует нашим ожиданиям.

Комментарии:

1. Это действительно помогает ввести мои насмешки в мой контроллер, но у меня нет возможности инициализировать мои насмешки до их введения, поэтому конструктор контроллера не видит макетные данные.

Ответ №3:

Для реализации «модульный тест» имеет мало значения @Controller . Если вы используете @WebMvcTest , то вы можете использовать @MockBean :

 @WebMvcTest
class MyControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private MyRepository repository;

  @Test
  void testSomething() {
    mockMvc.perform( ... );
  }
}
 

Комментарии:

1. Почему бы мне не захотеть провести модульное тестирование своих контроллеров? В них у меня есть нетривиальная бизнес-логика. В традиционных моделях MVC большая часть логики должна находиться на уровне контроллера.

2. Потому что у контроллеров обычно много аннотаций, в которых вы также хотите их протестировать. Это проще всего использовать @WebMvcTest . Обычно контроллеры не должны иметь в себе бизнес-логики. Они должны иметь дело только с преобразованием HTTP. Я размещаю бизнес-логику на уровне сервисов.