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
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
You simply need to
@SpringBootTest(args = "test")
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>
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.
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 ...
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");
}
}