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

34,341

Solution 1

@SpringBootTest has args param. You can pass cli arguments there

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

Solution 2

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

}

Solution 3

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

Solution 4

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>

Solution 5

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

Share:
34,341
divinedragon
Author by

divinedragon

I love code and I write them a lot.

Updated on July 09, 2022

Comments

  • divinedragon
    divinedragon almost 2 years

    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.

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

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class CityApplicationTests {
    
        @Test
        public void contextLoads() {
        }
    }
    

    Now, Assert.notEmpty() mandates for passing an argument. However, now, I am writing JUnit test for the same. But, I get following exception raise from the 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?