Using junit test to pass command line argument to Spring Boot application

前端 未结 7 1538
抹茶落季
抹茶落季 2021-02-12 19:07

I have a very basic Spring Boot application, which is expecting an argument from command line, and without it doesn\'t work. Here is the code.

@SpringBootApplica         


        
相关标签:
7条回答
  • 2021-02-12 19:46

    I'm affraid that your solution will not work in a way that you presented (until you implement your own test framework for Spring).

    This is because when you are running tests, Spring (its test SpringBootContextLoader to be more specific) runs your application in its own way. It instantiates SpringApplication and invokes its run method without any arguments. It also never uses your main method implemented in application.

    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. (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)

    Reading parameters using @Value annotation:

    @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

    0 讨论(0)
  • 2021-02-12 19:47

    You simply need to

    @SpringBootTest(args = "test")

    0 讨论(0)
  • 2021-02-12 19:50

    I would leave SpringBoot out of the equation.

    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 ? I suppose, the purpose of this test is more for regression, ensuring that your application always throws an IllegalArgumentException when no args are provided? 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:

    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-all</artifactId>
        <version>1.9.0</version>
        <scope>test</scope>
    </dependency>
    
    0 讨论(0)
  • 2021-02-12 19:54

    As mentioned in this answer, Spring Boot currently doesn't offer a way to intercept/replace the DefaultApplicationArguments that it uses. 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:

    @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.

    0 讨论(0)
  • 2021-02-12 19:56

    In your code autowire springs ApplicationArguments. Use getSourceArgs() to retrieve the commandline arguments.

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

    In your test mock the 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. 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. Then all your classes only need to use springs Environment properties.

    E.g.

    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. Since its run() method alwyas gets executed right after the creation of the spring context. Even in a test-class. So it will run, before your Test started ...

    0 讨论(0)
  • 2021-02-12 19:58

    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.

    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");
        }
    
    }
    
    0 讨论(0)
提交回复
热议问题