it-swarm.com.de

Mit junit test können Sie das Befehlszeilenargument an die Spring Boot-Anwendung übergeben

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?

10
divinedragon

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

}
5

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

3
Maciej Marczuk

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>
3
alexbt

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

1
Torsten

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.

0
itzg