Иногда в приложениях полезно иметь консоль для управления приложением непосредственно с сервера. Одним из чрезвычайно удобных решений данной задачи является 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
Скрестим пальцы. Запускаем тест. Работает.
Дело раскрыто, спасибо за внимание.