简体   繁体   English

使用 junit 测试将命令行参数传递给 Spring Boot 应用程序

[英]Using junit test to pass command line argument to Spring Boot application

I have a very basic Spring Boot application, which is expecting an argument from command line, and without it doesn't work.我有一个非常基本的 Spring Boot 应用程序,它需要来自命令行的参数,没有它就不起作用。 Here is the code.这是代码。

@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);
    }
}

Here is my JUnit test class.这是我的 JUnit 测试类。

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

    @Test
    public void contextLoads() {
    }
}

Now, Assert.notEmpty() mandates for passing an argument.现在, Assert.notEmpty()要求传递参数。 However, now, I am writing JUnit test for the same.但是,现在,我正在为此编写 JUnit 测试。 But, I get following exception raise from the Assert .但是,我从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

Any idea, how to pass the parameter?任何想法,如何传递参数?

I´ve managed to find a way to create Junit tests that worked fine with SpringBoot by injecting the ApplicationContext in my test and calling a CommandLineRunner with the required parameters.通过在我的测试中注入 ApplicationContext 并使用所需的参数调用 CommandLineRunner,我设法找到了一种创建 Junit 测试的方法,该测试与 SpringBoot 配合良好。

The final code looks like that:最终代码如下所示:

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");
    }

}

I'm affraid that your solution will not work in a way that you presented (until you implement your own test framework for Spring).我担心您的解决方案不会以您提供的方式工作(直到您为 Spring 实现自己的测试框架)。

This is because when you are running tests, Spring (its test SpringBootContextLoader to be more specific) runs your application in its own way.这是因为当您运行测试时,Spring(它的测试SpringBootContextLoader更具体)以自己的方式运行您的应用程序。 It instantiates SpringApplication and invokes its run method without any arguments.它实例化SpringApplicationrun没有任何参数的情况下调用其run方法。 It also never uses your main method implemented in application.它也从不使用您在应用程序中实现的main方法。

However, you could refactor your application in a way that it'll be possible to test it.但是,您可以以一种可以对其进行测试的方式重构您的应用程序。

I think (since you are using Spring) the easiest solution could be implemented using spring configuration properties instead of pure command line arguments.我认为(因为您使用的是 Spring)最简单的解决方案可以使用 spring 配置属性而不是纯命令行参数来实现。 (But you should be aware that this solution should be used rather for "configuration arguments", because that's the main purpose of springs configuration properties mechanism) (但你应该知道这个解决方案应该用于“配置参数”,因为这是 springs configuration properties机制的主要目的)

Reading parameters using @Value annotation:使用@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);
        //...
    }
}

Sample test:样品测试:

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

    @Test
    public void contextLoads() {
    }
}

And when running your command line app just add your custom params:在运行命令行应用程序时,只需添加自定义参数:

--myCustomArgs.customArg1=testValue

I would leave SpringBoot out of the equation.我会将 SpringBoot 排除在等式之外。

You simply need to test the run method, without going through Spring Boot, since your goal is not to test spring boot, isn't it ?您只需要测试run方法,而无需通过 Spring Boot,因为您的目标不是测试 spring boot,不是吗? I suppose, the purpose of this test is more for regression, ensuring that your application always throws an IllegalArgumentException when no args are provided?我想,这个测试的目的更多是为了回归,确保你的应用程序在没有提供参数时总是抛出IllegalArgumentException Good old unit test still works to test a single method:好的旧单元测试仍然可以测试单个方法:

@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);
    }
}

I used Mockito to inject mocks for Reader and Writer:我使用 Mockito 为 Reader 和 Writer 注入模拟:

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

In your code autowire springs ApplicationArguments .在您的代码自动装配弹簧ApplicationArguments Use getSourceArgs() to retrieve the commandline arguments.使用getSourceArgs()检索命令行参数。

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

In your test mock the ApplicationArguments.在您的测试中模拟 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"));

    }
}

Like Maciej Marczuk suggested, I also prefer to use Springs Environment properties instead of commandline arguments.就像Maciej Marczuk建议的那样,我也更喜欢使用 Springs Environment属性而不是命令行参数。 But if you cannot use the springs syntax --argument=value you could write an own PropertySource , fill it with your commandline arguments syntax and add it to the ConfigurableEnvironment .但是,如果您不能使用 springs 语法--argument=value您可以编写自己的PropertySource ,用命令行参数语法填充它并将其添加到ConfigurableEnvironment Then all your classes only need to use springs Environment properties.那么你所有的类只需要使用 springs Environment 属性。

Eg例如

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);
    }
}

BTW:顺便说一句:

Since I do not have enough reputation points to comment an answer, I would still like to leave a hard learned lesson here:由于我没有足够的声望点来评论答案,我仍然想在这里留下一个深刻的教训:

The CommandlineRunner is not such a good alternative. CommandlineRunner不是一个很好的选择。 Since its run() method alwyas gets executed right after the creation of the spring context.由于它的run()方法 alwyas 在创建 spring 上下文之后立即执行。 Even in a test-class.即使在考试班。 So it will run, before your Test started ...所以它会在你的测试开始之前运行......

You simply need to你只需要

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

As mentioned in this answer , Spring Boot currently doesn't offer a way to intercept/replace the DefaultApplicationArguments that it uses.本答案所述,Spring Boot 目前不提供拦截/替换它使用的DefaultApplicationArguments 的方法。 A natural-Boot-way that I used to solve this was to enhance my runner logic and use some autowired properties.我用来解决这个问题的一种自然引导方式是增强我的跑步者逻辑并使用一些自动装配的属性。

First, I created a properties component:首先,我创建了一个属性组件:

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

...autowired the properties component into my runner: ...将属性组件自动装配到我的跑步者中:

@Service
public class Loader implements ApplicationRunner {

    private AppProperties properties;

    @Autowired
    public Loader(AppProperties properties) {
        this.properties = properties;
    }
    ...

...and, in the run I only assert'ed when that property is enabled, which is defaulted to true for normal application usage: ...而且,在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);
    }
}

With that, I can tweak those properties to execute in a unit test friendly manner:有了这个,我可以调整这些属性以以单元测试友好的方式执行:

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

    @Test
    public void contextLoads() {
    }

}

I needed the exitWhenFinished part since my particular runner normally calls System.exit(0) and exiting that way leaves the unit test in a semi-failed state.我需要exitWhenFinished部分,因为我的特定运行程序通常调用System.exit(0)并且以这种方式退出会使单元测试处于半失败状态。

In Junit 5, simply we can pass commanline arguments using args parm present in @SpringBootTest(args = "20210831")在 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 {

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM