【问题标题】:Using junit test to pass command line argument to Spring Boot application使用 junit test 将命令行参数传递给 Spring Boot 应用程序
【发布时间】:2017-01-01 19:38:21
【问题描述】:

我有一个非常基本的 Spring Boot 应用程序,它需要来自命令行的参数,没有它就无法工作。这是代码。

@SpringBootApplication
public class Application implements CommandLineRunner {

    private static final Logger log = LoggerFactory.getLogger(Application.class);

    @Autowired
    private Reader reader;

    @Autowired
    private Writer writer;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) throws Exception {

        Assert.notEmpty(args);

        List<> cities = reader.get("Berlin");
         writer.write(cities);
    }
}

这是我的 JUnit 测试类。

@RunWith(SpringRunner.class)
@SpringBootTest
public class CityApplicationTests {

    @Test
    public void contextLoads() {
    }
}

现在,Assert.notEmpty() 要求传递参数。但是,现在,我正在为此编写 JUnit 测试。但是,我从Assert 得到以下异常引发。

2016-08-25 16:59:38.714 ERROR 9734 --- [           main] o.s.boot.SpringApplication               : Application startup failed

java.lang.IllegalStateException: Failed to execute CommandLineRunner
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:801) ~[spring-boot-1.4.0.RELEASE.jar:1.4.0.RELEASE]
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:782) ~[spring-boot-1.4.0.RELEASE.jar:1.4.0.RELEASE]
    at org.springframework.boot.SpringApplication.afterRefresh(SpringApplication.java:769) ~[spring-boot-1.4.0.RELEASE.jar:1.4.0.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) ~[spring-boot-1.4.0.RELEASE.jar:1.4.0.RELEASE]
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:111) [spring-boot-test-1.4.0.RELEASE.jar:1.4.0.RELEASE]
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:117) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:83) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.boot.test.autoconfigure.AutoConfigureReportTestExecutionListener.prepareTestInstance(AutoConfigureReportTestExecutionListener.java:46) [spring-boot-test-autoconfigure-1.4.0.RELEASE.jar:1.4.0.RELEASE]
    at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:230) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:228) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:287) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) [junit-4.12.jar:4.12]
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:289) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:247) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) [junit-4.12.jar:4.12]
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) [junit-4.12.jar:4.12]
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) [junit-4.12.jar:4.12]
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) [junit-4.12.jar:4.12]
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) [junit-4.12.jar:4.12]
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363) [junit-4.12.jar:4.12]
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) [.cp/:na]
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) [.cp/:na]
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) [.cp/:na]
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678) [.cp/:na]
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382) [.cp/:na]
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192) [.cp/:na]
Caused by: java.lang.IllegalArgumentException: [Assertion failed] - this array must not be empty: it must contain at least 1 element
    at org.springframework.util.Assert.notEmpty(Assert.java:222) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.util.Assert.notEmpty(Assert.java:234) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.deepakshakya.dev.Application.run(Application.java:33) ~[classes/:na]
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798) ~[spring-boot-1.4.0.RELEASE.jar:1.4.0.RELEASE]
    ... 32 common frames omitted

任何想法,如何传递参数?

【问题讨论】:

  • 说实话,你还想测试一下吗?我只想创建一个额外的配置文件,在使用该配置文件时排除 bean,在集成测试中使用该配置文件并完成。要测试run() 方法的功能,您可以随时编写单元测试,而不必为此启动应用程序。
  • 另外...你没有展示你是如何开始测试的,这让我们不知道你是如何尝试传递参数的。
  • 我已经更新了调用应用程序的 JUnit 类。
  • 您应该至少指定一个command line 参数。
  • @AntonNovopashin 我想提供,但这就是问题所在。我应该在哪里指定。我可以在 Eclipse 中运行它,但如果我想在 CI 上运行它呢?

标签: java spring unit-testing junit spring-boot


【解决方案1】:

在 Junit 5 中,我们可以简单地使用 @SpringBootTest(args = "20210831") 中的 args parm 传递命令行参数

@ExtendWith(SpringExtension.class)
@SpringBootTest(args = "20210831")
@TestPropertySource("/application-test.properties")
@ContextConfiguration(classes = {Test.class, TestConfig.class})
public class AppleTest {

【讨论】:

    【解决方案2】:

    你只需要

    @SpringBootTest(args = "test")
    class YourApplicationTests {
      @Test
      public void contextLoads() {
      }
    }
    

    【讨论】:

    • 您可以在 Spring Boot 应用程序中使用 if (args.length==0 || !args[0].equals("test"))
    • 请使用答案下方的edit 按钮将所有信息放在那里,然后将代码元素格式化为代码,以便它们更具可读性。 editing guide 可以在这里为您提供帮助。
    【解决方案3】:

    @SpringBootTest 具有 args 参数。您可以在那里传递 cli 参数

    https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/context/SpringBootTest.html#args--

    【讨论】:

    • springboot版本应该在2.2.0以上
    【解决方案4】:

    在您的代码中自动装配弹簧ApplicationArguments。使用getSourceArgs() 检索命令行参数。

    public CityApplicationService(ApplicationArguments args, Writer writer){        
        public void writeFirstArg(){
            writer.write(args.getSourceArgs()[0]);
        }
    }
    

    在您的测试中模拟 ApplicationArguments。

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class CityApplicationTests {
    @MockBean
    private ApplicationArguments args;
    
        @Test
        public void contextLoads() {
            // given
            Mockito.when(args.getSourceArgs()).thenReturn(new String[]{"Berlin"});
    
            // when
            ctx.getBean(CityApplicationService.class).writeFirstArg();
    
            // then
            Mockito.verify(writer).write(Matchers.eq("Berlin"));
    
        }
    }
    

    Maciej Marczuk 建议的那样,我也更喜欢使用 Springs Environment 属性而不是命令行参数。但是如果你不能使用 springs 语法--argument=value,你可以编写一个自己的PropertySource,用你的命令行参数语法填充它并将它添加到ConfigurableEnvironment。那么你所有的类只需要使用 springs 环境属性即可。

    例如

    public class ArgsPropertySource extends PropertySource<Map<String, String>> {
    
        ArgsPropertySource(List<CmdArg> cmdArgs, List<String> arguments) {
            super("My special commandline arguments", new HashMap<>());
    
            // CmdArgs maps the property name to the argument value.
            cmdArgs.forEach(cmd -> cmd.mapArgument(source, arguments));
        }
    
        @Override
        public Object getProperty(String name) {
            return source.get(name);
        }
    }
    
    
    public class SetupArgs {
    
        SetupArgs(ConfigurableEnvironment env, ArgsMapping mapping) {           
            // In real world, this code would be in an own method.
            ArgsPropertySource = new ArgsPropertySource(mapping.get(), args.getSourceArgs());
            environment
                .getPropertySources()
                .addFirst(propertySource);
        }
    }
    

    顺便说一句:

    由于我没有足够的声望点来评论答案,我仍然想在这里留下一个艰难的教训:

    CommandlineRunner 不是一个很好的选择。因为它的run() 方法总是在创建弹簧上下文后立即执行。即使在测试班。所以它会在你的测试开始之前运行......

    【讨论】:

      【解决方案5】:

      我设法通过在我的测试中注入 ApplicationContext 并使用所需参数调用 CommandLineRunner 来创建与 SpringBoot 配合良好的 Junit 测试。

      最终代码如下:

      package my.package.
      
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.CommandLineRunner;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.context.ApplicationContext;
      import org.springframework.test.context.junit4.SpringRunner;
      
      @RunWith(SpringRunner.class)
      @SpringBootTest
      class AsgardBpmClientApplicationIT {
      
          @Autowired
          ApplicationContext ctx;
      
          @Test
          public void testRun() {
              CommandLineRunner runner = ctx.getBean(CommandLineRunner.class);
              runner.run ( "-k", "arg1", "-i", "arg2");
          }
      
      }
      

      【讨论】:

      • 这是一个很好的解决方案。不过测试需要公开。
      • 一条评论/声明,如果不采取任何措施避免,命令行将在调用之前“加载”,如果需要参数,则初始化失败。
      • 不要自动装配 ApplicationContext。只需自动装配 CommandLineRunner。
      • @SvenDöring:你能提供一个简单的例子吗?
      • 而不是@Autowired ApplicationContext ctx; 只需使用:@Autowired YourCommandLineRunner runner;
      【解决方案6】:

      in this answer 所述,Spring Boot 目前不提供拦截/替换它使用的DefaultApplicationArguments 的方法。我用来解决这个问题的一种自然引导方式是增强我的跑步者逻辑并使用一些自动装配的属性。

      首先,我创建了一个属性组件:

      @ConfigurationProperties("app") @Component @Data
      public class AppProperties {
          boolean failOnEmptyFileList = true;
          boolean exitWhenFinished = true;
      }
      

      ...将属性组件自动装配到我的跑步者中:

      @Service
      public class Loader implements ApplicationRunner {
      
          private AppProperties properties;
      
          @Autowired
          public Loader(AppProperties properties) {
              this.properties = properties;
          }
          ...
      

      ...并且,在run 中,我仅在启用该属性时才断言,对于正常的应用程序使用默认为true

      @Override
      public void run(ApplicationArguments args) throws Exception {
          if (properties.isFailOnEmptyFileList()) {
              Assert.notEmpty(args.getNonOptionArgs(), "Pass at least one filename on the command line");
          }
      
          // ...do some loading of files and such
      
          if (properties.isExitWhenFinished()) {
              System.exit(0);
          }
      }
      

      这样,我可以调整这些属性以对单元测试友好的方式执行:

      @RunWith(SpringRunner.class)
      @SpringBootTest(properties = {
              "app.failOnEmptyFileList=false",
              "app.exitWhenFinished=false"
      })
      public class InconsistentJsonApplicationTests {
      
          @Test
          public void contextLoads() {
          }
      
      }
      

      我需要 exitWhenFinished 部分,因为我的特定跑步者通常会调用 System.exit(0) 并以这种方式退出会使单元测试处于半失败状态。

      【讨论】:

        【解决方案7】:

        我担心您的解决方案不会以您提出的方式工作(直到您实现自己的 Spring 测试框架)。

        这是因为当您运行测试时,Spring(更具体地说是它的测试 SpringBootContextLoader)以自己的方式运行您的应用程序。它实例化SpringApplication 并调用它的run 方法,不带任何参数。它也从不使用您在应用程序中实现的 main 方法。

        但是,您可以重构您的应用程序,以便对其进行测试。

        我认为(因为您使用的是 Spring)最简单的解决方案可以使用 spring 配置属性而不是纯命令行参数来实现。 (但你应该知道,这个解决方案应该用于“配置参数”,因为这是 springs configuration properties 机制的主要目的)

        使用@Value注解读取参数:

        @SpringBootApplication
        public class Application implements CommandLineRunner {
        
            @Value("${myCustomArgs.customArg1}")
            private String customArg1;
        
            public static void main(String[] args) {
                SpringApplication.run(Application.class, args);
            }
        
            @Override
            public void run(String... args) throws Exception {
        
                Assert.notNull(customArg1);
                //...
            }
        }
        

        样本测试:

        @RunWith(SpringRunner.class)
        @SpringBootTest({"myCustomArgs.customArg1=testValue"})
        public class CityApplicationTests {
        
            @Test
            public void contextLoads() {
            }
        }
        

        当运行你的命令行应用程序时,只需添加你的自定义参数:

        --myCustomArgs.customArg1=testValue

        【讨论】:

          【解决方案8】:

          我会把 SpringBoot 排除在外。

          您只需要测试run 方法,而无需通过 Spring Boot,因为您的目标不是测试 Spring Boot,不是吗? 我想,这个测试的目的更多是为了回归,确保你的应用程序在没有提供参数时总是抛出一个IllegalArgumentException?好的旧单元测试仍然可以测试单个方法:

          @RunWith(MockitoJUnitRunner.class)
          public class ApplicationTest {
          
              @InjectMocks
              private Application app = new Application();
          
              @Mock
              private Reader reader;
          
              @Mock
              private Writer writer;
          
              @Test(expected = IllegalArgumentException.class)
              public void testNoArgs() throws Exception {
                  app.run();
              }
          
              @Test
              public void testWithArgs() throws Exception {
                  List list = new ArrayList();
                  list.add("test");
                  Mockito.when(reader.get(Mockito.anyString())).thenReturn(list);
          
                  app.run("myarg");
          
                  Mockito.verify(reader, VerificationModeFactory.times(1)).get(Mockito.anyString());
                  Mockito.verify(writer, VerificationModeFactory.times(1)).write(list);
              }
          }
          

          我使用 Mockito 为 Reader 和 Writer 注入 mock:

          <dependency>
              <groupId>org.mockito</groupId>
              <artifactId>mockito-all</artifactId>
              <version>1.9.0</version>
              <scope>test</scope>
          </dependency>
          

          【讨论】:

          • 同意你的观点。但是,我更多地从功能测试的角度来看,并希望通过从命令行提供不同的值来专门测试代码的执行。好像没有什么办法。
          猜你喜欢
          • 2020-12-13
          • 1970-01-01
          • 2019-06-23
          • 1970-01-01
          • 2018-05-27
          • 2016-10-25
          • 2020-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多