У меня есть тестовая утилита, для которой мне нужен новый экземпляр для каждого метода тестирования (чтобы предотвратить утечку этого состояния между тестами). До сих пор я использовал область «прототип», но теперь я хочу иметь возможность подключить утилиту к другой тестовой утилите, и подключенные экземпляры должны быть одинаковыми для каждого теста.
Кажется, это стандартная проблема, поэтому мне было интересно, есть ли область «метода тестирования» или что-то подобное?
Это структура тестового класса и тестовых утилит:
@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTest {
@Autowired
private TestDriver driver;
@Autowired
private TestStateProvider state;
// ... state
// ... methods
}
@Component
@Scope("prototype") // not right because MyTest and TestStateProvider get separate instances
public class TestDriver {
// ...
}
@Component
public class TestStateProvider {
@Autowired
private TestDriver driver;
// ...
}
Я знаю, что мог бы использовать @Scope("singleton")
и @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
, но это обновляет больше, чем мне нужно — нового экземпляра TestDriver
для каждого теста будет достаточно. Кроме того, этот подход подвержен ошибкам, потому что все тесты, использующие TestDriver
, должны знать, что им также нужна аннотация @DirtiesContext
. Поэтому я ищу лучшее решение.
Некоторое время назад я столкнулся с той же проблемой и пришел к следующему решению:
Поэтому создайте класс TestConfiguration со следующими методами и определением bean-компонента.
private MockSettings createResetAfterMockSettings() {
return MockReset.withSettings(MockReset.AFTER);
}
private <T> T mockClass(Class<T> classToMock) {
return mock(classToMock, createResetAfterMockSettings());
}
и ваше определение компонента будет выглядеть так:
@Bean
public TestDriver testDriver() {
return mockClass(TestDriver .class);
}
MockReset.AFTER
используется для сброса макета после запуска метода тестирования.
И, наконец, добавьте TestExecutionListeners
в свой тестовый класс:
@TestExecutionListeners({ResetMocksTestExecutionListener.class})
На самом деле довольно легко реализовать testMethod
область видимости:
public class TestMethodScope implements Scope {
public static final String NAME = "testMethod";
private Map<String, Object> scopedObjects = new HashMap<>();
private Map<String, Runnable> destructionCallbacks = new HashMap<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
if (!scopedObjects.containsKey(name)) {
scopedObjects.put(name, objectFactory.getObject());
}
return scopedObjects.get(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
destructionCallbacks.put(name, callback);
}
@Override
public Object remove(String name) {
throw new UnsupportedOperationException();
}
@Override
public String getConversationId() {
return null;
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
public static class TestExecutionListener implements org.springframework.test.context.TestExecutionListener {
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) testContext
.getApplicationContext();
TestMethodScope scope = (TestMethodScope) applicationContext.getBeanFactory().getRegisteredScope(NAME);
scope.destructionCallbacks.values().forEach(callback -> callback.run());
scope.destructionCallbacks.clear();
scope.scopedObjects.clear();
}
}
@Component
public static class ScopeRegistration implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
factory.registerScope(NAME, new TestMethodScope());
}
}
}
Просто зарегистрируйте прослушиватель выполнения теста, и для каждого теста будет один экземпляр всех @Scope("testMethod")
аннотированных типов:
@RunWith(SpringRunner.class)
@SpringBootTest
@TestExecutionListeners(listeners = TestMethodScope.TestExecutionListener.class,
mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
public class MyTest {
@Autowired
// ... types annotated with @Scope("testMethod")
}
Я вижу, как это будет работать, если я захочу сбросить состояние макетов (например, счетчик вызовов метода), но
TestDriver
— это реальный класс реализации, и я хочу сбросить состояние в этой реализации. Это не сработает, не так ли?