Ich habe eine sehr einfache Spring Boot-Anwendung, die ein Argument von der Kommandozeile erwartet und ohne es funktioniert nicht. Hier ist der 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);
}
}
Hier ist meine JUnit-Testklasse.
@RunWith(SpringRunner.class)
@SpringBootTest
public class CityApplicationTests {
@Test
public void contextLoads() {
}
}
Nun muss Assert.notEmpty()
ein Argument übergeben. Jetzt schreibe ich jedoch JUnit-Test für dasselbe. Ich erhalte jedoch folgende Ausnahmebedingungen von der 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
Irgendeine Idee, wie man den Parameter weitergibt?
Ich habe einen Weg gefunden, Junit-Tests zu erstellen, die gut mit SpringBoot zusammenarbeiteten, indem der ApplicationContext in meinen Test eingefügt und ein CommandLineRunner mit den erforderlichen Parametern aufgerufen wurde.
Der endgültige Code sieht so aus:
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");
}
}
Ich befürchte, dass Ihre Lösung nicht auf eine Weise funktioniert, die Sie vorgestellt haben (bis Sie Ihr eigenes Testframework für Spring implementiert haben).
Wenn Sie Tests ausführen, führt Spring (genauer gesagt der Test SpringBootContextLoader
) Ihre Anwendung auf ihre eigene Weise aus. Es instantiiert SpringApplication
und ruft die run
-Methode ohne Argumente auf. Es verwendet auch niemals Ihre in der Anwendung implementierte main
-Methode.
Sie können Ihre Anwendung jedoch so umgestalten, dass Sie sie testen können.
Ich denke (da Sie Spring verwenden) könnte die einfachste Lösung mithilfe von Federkonfigurationseigenschaften anstelle von reinen Befehlszeilenargumenten implementiert werden. (Sie sollten sich jedoch bewusst sein, dass diese Lösung eher für "Konfigurationsargumente" verwendet werden sollte, da dies der Hauptzweck des configuration properties
-Mechanismus von Springs ist.)
Parameter mit der @Value
-Annotation lesen:
@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);
//...
}
}
Beispieltest:
@RunWith(SpringRunner.class)
@SpringBootTest({"myCustomArgs.customArg1=testValue"})
public class CityApplicationTests {
@Test
public void contextLoads() {
}
}
Und wenn Sie Ihre Befehlszeilen-App ausführen, fügen Sie einfach Ihre benutzerdefinierten Parameter hinzu:
--myCustomArgs.customArg1=testValue
Ich würde SpringBoot aus der Gleichung herausnehmen.
Sie müssen lediglich die run
-Methode testen, ohne Spring Boot zu durchlaufen, da es nicht Ihr Ziel ist, Spring Boot zu testen, nicht wahr? wirft immer eine IllegalArgumentException
, wenn keine Argumente bereitgestellt werden? Der gute alte Gerätetest funktioniert immer noch, um eine einzelne Methode zu testen:
@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);
}
}
Ich habe Mockito verwendet, um Mocks für Reader und Writer zu injizieren:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.0</version>
<scope>test</scope>
</dependency>
In Ihrem Code autowire Springs ApplicationArguments
. Verwenden Sie getSourceArgs()
, um die Befehlszeilenargumente abzurufen.
public CityApplicationService(ApplicationArguments args, Writer writer){
public void writeFirstArg(){
writer.write(args.getSourceArgs()[0]);
}
}
In Ihrem Test simulieren Sie die 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"));
}
}
Wie Maciej Marczuk vorgeschlagen, bevorzuge ich auch die Verwendung von Springs Environment
-Eigenschaften anstelle von Befehlszeilenargumenten. Wenn Sie die Federn-Syntax --argument=value
nicht verwenden können, können Sie eine eigene PropertySource
schreiben, diese mit Ihrer Befehlszeilenargumentensyntax füllen und der ConfigurableEnvironment
hinzufügen. Dann müssen alle Ihre Klassen nur Federeigenschaften verwenden.
Z.B.
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:
Da ich nicht genug Reputationspunkte habe, um eine Antwort zu kommentieren, möchte ich hier noch eine hart erlernte Lektion hinterlassen:
Die CommandlineRunner
ist keine so gute Alternative. Seit der run()
-Methode wird alwyas direkt nach der Erstellung des Federkontextes ausgeführt. Sogar in einer Testklasse. So wird es laufen, bevor der Test gestartet ist ...
Wie in dieser Antwort erwähnt, bietet Spring Boot derzeit keine Möglichkeit, die DefaultApplicationArguments , die es verwendet, abzufangen/zu ersetzen. Ein natürlicher Boot-Weg, den ich zur Lösung dieses Problems verwendete, bestand darin, meine Läuferlogik zu verbessern und einige autarke Eigenschaften zu verwenden.
Zuerst habe ich eine Eigenschaftskomponente erstellt:
@ConfigurationProperties("app") @Component @Data
public class AppProperties {
boolean failOnEmptyFileList = true;
boolean exitWhenFinished = true;
}
... die Eigenschaftskomponente automatisch an meinen Läufer angeschlossen:
@Service
public class Loader implements ApplicationRunner {
private AppProperties properties;
@Autowired
public Loader(AppProperties properties) {
this.properties = properties;
}
...
... und in der Variable run
bestätige ich nur, wenn diese Eigenschaft aktiviert ist, die standardmäßig für die normale Anwendung true
verwendet wird:
@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);
}
}
Damit kann ich diese Eigenschaften so anpassen, dass sie in einem Unit-Test-freundlichen Modus ausgeführt werden:
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
"app.failOnEmptyFileList=false",
"app.exitWhenFinished=false"
})
public class InconsistentJsonApplicationTests {
@Test
public void contextLoads() {
}
}
Ich brauchte den exitWhenFinished
-Teil, da mein spezieller Läufer normalerweise System.exit(0)
aufruft und auf diese Weise den Unit-Test in einem halb ausgefallenen Zustand verlässt.