Иногда в приложениях полезно иметь консоль для управления приложением непосредственно с сервера. Одним из чрезвычайно удобных решений данной задачи является Spring Shell.
Тесты — тоже весьма неплохая практика (надеюсь у вас они есть) и, иногда, они пишутся с аннотацией @SpringBootTest. Однако, если вы подключите Spring Shell и попробуете запустить такой тест, то… ваш тест просто зависнет в ожидании введения команды с консоли.
Итак, отправляемся на поиски решения.
Гуглим
После недолгого поиска на GitHub находим похожую проблему.
Автор предлагает для тестирования shell-а переопределить бин с типом ApplicationRunner, который и ожидает команды с консоли. Здесь же решение по доступу и тестированию самих команд определенных в @ShellComponent.
@Component
public class CliAppRunner implements ApplicationRunner {
public CliAppRunner() {
}
@Override
public void run(ApplicationArguments args) throws Exception {
//do nothing
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes =CliConfig.class)
public class ShellCommandIntegrationTest {
@Autowired
private Shell shell;
@Test
public void runTest(){
Object result=shell.evaluate(new Input(){
@Override
public String rawText() {
return "add 1 3";
}
});
DefaultResultHandler resulthandler=new DefaultResultHandler();
resulthandler.handleResult(result);
}
}
К сожалению тесты при таком решении все равно зависают в ожидании команды.
Пришло время заглянуть под капот!
После легкого дебага находим в классе SpringApplication следующий код:
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
Иначе говоря, Spring Boot просто добавляет наш бин с кастомным ApplicationRunner в копилку к уже определенным и запускает их все.
Казалось бы решение простое — переопределим бин! Время залезть в исходники Spring Shell.
Переопределяем бин
Быстро выясняется, что за создание раннеров отвечает класс JLineShellAutoConfiguration, конкретно нас интересует бин scriptApplicationRunner, который и не дает нашему тесту запуститься.
Ок, переопределим его в нашем тестовом классе (не забыв включить spring.main.allow-bean-definition-overriding=true для Spring 2.+):
@TestConfiguration
static class Runner {
@Bean
public ApplicationRunner scriptApplicationRunner(){
return new CliAppRunner();
}
}
Нет, опять не сработало. JLineShellAutoConfiguration подгружается позже нашей тестовой конфигурации Runner и успешно переопределяет scriptApplicationRunner. И тест опять не запускается (Небольшой интерактив — кто-нибудь — объясните в комментариях, почему так?).
Ищем другие варианты
Что ж, посмотрим, что там написано в создании бина в JLineShellAutoConfiguration:
@Bean
@ConditionalOnProperty(prefix = SPRING_SHELL_SCRIPT, value = ScriptShellApplicationRunner.ENABLED, havingValue = "true", matchIfMissing = true)
public ApplicationRunner scriptApplicationRunner(Parser parser, ConfigurableEnvironment environment) {
return new ScriptShellApplicationRunner(parser, shell, environment);
}
Ура, нам повезло — есть property, который позволяет его отключить. Радостно бежим вписывать его в application.properties:
spring.shell.script.enabled=false
Запускаем наш тест. И он опять зависает. Копаем дальше.
Разгадка
Идем в ScriptShellApplicationRunner и смотрим, что там с нашими property. А там:
public static final String SPRING_SHELL_SCRIPT = "spring.shell.script";
public static final String ENABLED = "spring.shell.script";
/**
* The name of the environment property that allows to disable the behavior of this
* runner.
*/
public static final String SPRING_SHELL_SCRIPT_ENABLED = SPRING_SHELL_SCRIPT + "." + ENABLED;
Воу, кажется теперь все понятно — идем снова в application.properties и пишем:
spring.shell.script.spring.shell.script=false
Скрестим пальцы. Запускаем тест. Работает.
Дело раскрыто, спасибо за внимание.