web-dev-qa-db-fra.com

Utilisation de junit test pour passer l'argument de ligne de commande à l'application Spring Boot

J'ai une application Spring Boot très basique, qui attend un argument de la ligne de commande, et sans cela ne fonctionne pas. Voici le 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);
    }
}

Voici ma classe de test JUnit.

@RunWith(SpringRunner.class)
@SpringBootTest
public class CityApplicationTests {

    @Test
    public void contextLoads() {
    }
}

Maintenant, Assert.notEmpty() permet de transmettre un argument. Cependant, maintenant, j'écris le test JUnit pour le même. Mais, je reçois l'exception suivante avec la levée de la 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

Une idée, comment passer le paramètre?

10
divinedragon

J'ai réussi à trouver un moyen de créer des tests Junit qui fonctionnaient bien avec SpringBoot en injectant ApplicationContext dans mon test et en appelant CommandLineRunner avec les paramètres requis.

Le code final ressemble à ça:

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
Philippe Sevestre

Je crains que votre solution ne fonctionne pas comme vous l'avez présenté (jusqu'à ce que vous mettiez en place votre propre cadre de test pour Spring).

En effet, lorsque vous exécutez des tests, Spring (son test SpringBootContextLoader étant plus spécifique) exécute votre application à sa manière. Il instancie SpringApplication et appelle sa méthode run sans aucun argument. De plus, il n'utilise jamais votre méthode main implémentée dans l'application.

Cependant, vous pouvez refactoriser votre application de manière à pouvoir la tester. 

Je pense (puisque vous utilisez Spring) que la solution la plus simple pourrait être mise en œuvre en utilisant les propriétés de configuration de Spring au lieu d’arguments de ligne de commande purs. (Mais sachez que cette solution devrait plutôt être utilisée pour les "arguments de configuration", car c'est le but principal du mécanisme springs configuration properties)

Lecture des paramètres en utilisant l'annotation @Value:

@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);
        //...
    }
}

Échantillon test:

@RunWith(SpringRunner.class)
@SpringBootTest({"myCustomArgs.customArg1=testValue"})
public class CityApplicationTests {

    @Test
    public void contextLoads() {
    }
}

Et lorsque vous exécutez votre application de ligne de commande, ajoutez simplement vos paramètres personnalisés:

--myCustomArgs.customArg1=testValue

3
Maciej Marczuk

Je laisserais SpringBoot en dehors de l'équation.

Vous devez simplement tester la méthode run, sans passer par Spring Boot, car votre objectif n'est pas de tester Spring Boot, n'est-ce pas? Je suppose que le but de ce test est davantage de régression, en veillant à ce que jette toujours une IllegalArgumentException quand aucun argument n'est fourni? Le bon vieux test unitaire fonctionne toujours pour tester une seule méthode:

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

J'ai utilisé Mockito pour injecter des exemples pour Reader et Writer:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.9.0</version>
    <scope>test</scope>
</dependency>
3
alexbt

Dans votre code, autowire springs ApplicationArguments. Utilisez getSourceArgs() pour récupérer les arguments de la ligne de commande.

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

Dans votre test, simulez 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"));

    }
}

Comme Maciej Marczuk suggéré, je préfère également utiliser les propriétés Springs Environment au lieu des arguments en ligne de commande. Mais si vous ne pouvez pas utiliser la syntaxe springs --argument=value, vous pouvez écrire votre propre variable PropertySource, remplissez-la avec la syntaxe de votre argument de ligne de commande et ajoutez-la à la ConfigurableEnvironment. Toutes vos classes doivent alors uniquement utiliser les propriétés de l'environnement springs.

Par exemple.

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: 

Comme je n'ai pas assez de points de réputation pour commenter une réponse, j'aimerais quand même laisser ici une leçon difficile à apprendre:

La CommandlineRunner n'est pas une si bonne alternative. Depuis sa méthode run(), alwyas est exécuté juste après la création du contexte Spring. Même dans une classe de test. Donc, il fonctionnera avant le début de votre test ... 

1
Torsten

Comme mentionné dans cette réponse , Spring Boot n'offre actuellement aucun moyen d'intercepter/de remplacer le DefaultApplicationArguments qu'il utilise. Une solution naturelle utilisée par Boot pour résoudre ce problème consistait à améliorer la logique de mon coureur et à utiliser certaines propriétés auto-câblées.

Tout d'abord, j'ai créé un composant de propriétés:

@ConfigurationProperties("app") @Component @Data
public class AppProperties {
    boolean failOnEmptyFileList = true;
    boolean exitWhenFinished = true;
}

... automatiquement le composant de propriétés dans mon coureur:

@Service
public class Loader implements ApplicationRunner {

    private AppProperties properties;

    @Autowired
    public Loader(AppProperties properties) {
        this.properties = properties;
    }
    ...

... et, dans la run, je n'affirmais que lorsque cette propriété était activée, la valeur par défaut étant true pour une utilisation normale de l'application:

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

Avec cela, je peux Tweak ces propriétés à exécuter de manière conviviale pour les tests unitaires:

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
        "app.failOnEmptyFileList=false",
        "app.exitWhenFinished=false"
})
public class InconsistentJsonApplicationTests {

    @Test
    public void contextLoads() {
    }

}

J'avais besoin de la partie exitWhenFinished étant donné que mon coureur particulier appelle normalement System.exit(0) et que cette sortie laisse le test unitaire dans un état de demi-échec.

0
itzg