Comment tester la classe principale de l'application Spring-boot


J'ai une application spring-boot où ma classe de démarrage @SpringBootApplication ressemble à une classe standard. J'ai donc créé de nombreux tests pour toutes mes fonctionnalités et envoyé le résumé à sonarqube pour voir ma couverture.

Pour ma classe de démarrage Sonarqube me dit que je n'ai que 60% de couverture. Donc, la couverture moyenne n'est pas bonne comme prévu.

entrez la description de l'image ici

Ma classe de test est juste celle par défaut.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ElectronicGiftcardServiceApplication.class)
public class ElectronicGiftcardServiceApplicationTests {

    @Test
    public void contextLoads() {
    }
}

Alors, comment puis-je tester ma classe principale dans le démarreur classe de mon application?

Author: G. Ann - SonarSource Team, 2017-10-09

9 answers

Toutes ces réponses semblent exagérées.
Vous n'ajoutez pas de tests pour rendre un outil métrique heureux.
Chargement d'un Printemps contexte de l'application prend du temps. Ne l'ajoutez pas dans chaque build de développeur juste pour gagner environ 0,1% de couverture dans votre application.
Ici vous ne couvrez pas seulement 1 instruction de 1 méthode publique. Il ne représente rien en termes de couverture dans une application où des milliers de déclarations sont généralement écrites.

Première solution : faire votre classe d'application Spring Boot sans bean déclaré à l'intérieur. Si vous les avez, déplacez-les dans une classe de configuration (pour les faire encore couvrir par test unitaire). Et puis ignorez votre classe d'application Spring Boot dans la configuration de couverture de test .

Deuxième solution: si vous avez vraiment besoin de couvrir l'invocation main() (pour des raisons organisationnelles par exemple), créez un test pour cela mais un test d'intégration (exécuté par un outil d'intégration continue et non dans chaque developer build) et documenter clairement le but de la classe de test:

import org.junit.Test;

// Test class added ONLY to cover main() invocation not covered by application tests.
public class MyApplicationIT {
   @Test
   public void main() {
      MyApplication.main(new String[] {});
   }
}
 76
Author: davidxxx, 2018-08-10 09:21:54

Vous pouvez faire quelque chose comme ça

@Test
public void applicationContextLoaded() {
}

@Test
public void applicationContextTest() {
    mainApp.main(new String[] {});
}
 16
Author: fg78nc, 2017-10-09 15:52:44

J'avais le même objectif (avoir un test qui exécute la méthode main ()) et j'ai remarqué que simplement ajouter une méthode de test comme @fg78nc said "démarrera" l'application deux fois : une fois par spring boot test framework, une fois via l'invocation explicite de mainApp.main(new String[] {}), ce que je ne trouve pas élégant.

J'ai fini par écrire deux classes de test: une avec @SpringBootTest annotation et la méthode de test vide applicationContextLoaded () , une autre sans @SpringBootTest (seulement RunWith(SpringRunner.class)) qui appelle le principal méthode.

SpringBootApplicationTest

package example;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.boot.test.context.SpringBootTest;

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

  @Test
  public void contextLoads() {
  }
}

Application Starttest

package example;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
public class ApplicationStartTest {
  @Test
  public void applicationStarts() {
    ExampleApplication.main(new String[] {});
  }
}

Dans l'ensemble, l'application est toujours démarrée deux fois, mais parce qu'il y a maintenant deux classes de test. Bien sûr, avec seulement ces deux méthodes de tests, cela semble exagéré, mais généralement plus de tests seront ajoutés à la classe SpringBootApplicationTest en profitant de @SpringBootTest setup.

 7
Author: marcpa00, 2019-08-18 02:39:03
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <mainClass>your.awesome.package.Application</mainClass> 
    </configuration>
</plugin>

Si vous visez une couverture à 100%, une chose que vous pouvez faire est simplement de ne pas avoir de méthode principale du tout. Vous avez toujours besoin d'une classe annotée avec @SpringBootApplication mais elle peut être vide.

Soyez averti cependant car il a ses inconvénients et d'autres outils qui s'appuient sur main peuvent se casser.

 2
Author: Mariano LEANCE, 2018-03-22 18:08:17

Vous pouvez vous moquer de SpringApplication car il s'agit d'une dépendance de la méthode testée. Voir comment ici. C'est-à-dire

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.boot.SpringApplication;

import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;

@RunWith(PowerMockRunner.class)
public class ElectronicGiftcardServiceApplicationTest {

    @Test
    @PrepareForTest(SpringApplication.class)
    public void main() {
        mockStatic(SpringApplication.class);
        ElectronicGiftcardServiceApplication.main(new String[]{"Hello", "World"});
        verifyStatic(SpringApplication.class);
        SpringApplication.run(ElectronicGiftcardServiceApplication.class, new String[]{"Hello", "World"});
    }

}
 2
Author: awgtek, 2019-06-13 17:46:49

Même si cette question a reçu de nombreuses réponses, j'ai eu un cas d'utilisation qui n'est pas couvert ici et qui est peut-être intéressant à partager. Je valide certaines propriétés au démarrage et je voulais affirmer que l'application ne démarrerait pas si ces propriétés étaient mal configurées. En JUnit4, j'aurais pu faire quelque chose comme ceci:

@ActiveProfiles("incorrect")
@SpringBoot
public class NetworkProbeApplicationTest {

    @Test(expected=ConfigurationPropertiesBindException.class)
    public void contextShouldNotLoadWhenPropertiesIncorrect() {
    }
}

Mais dans JUnit5, vous ne pouvez plus ajouter la valeur "attendue" à votre annotation @Test et vous devez le faire différemment. Et puisque je voulais démarrez l'application avec un ensemble incorrect de propriétés dont j'avais besoin pour passer dans quel profil utiliser comme argument main (). Je ne pouvais pas vraiment trouver cela documenté nulle part, mais passer des arguments via la méthode main() vous oblige à préfixer vos arguments avec un double trait d'union et à séparer la clé et la valeur avec un signe égal. Un test complet ressemblerait à ceci:

import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindException;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class NetworkProbeApplicationTest {

    @Test
    public void contextShouldNotLoadWhenPropertiesIncorrect() {
        Exception exception = assertThrows(ConfigurationPropertiesBindException.class, () -> {
            SpringApplication.run(NetworkProbeApplication.class, "--spring.profiles.active=incorrect");
        });

        String expectedMessage = "Error creating bean with name 'dnsConfiguration': Could not bind properties to 'DnsConfiguration' : prefix=dns";

        assertTrue(exception.getMessage().contains(expectedMessage));
    }
}
 1
Author: Jonck van der Kogel, 2020-09-26 08:10:18

Ce test simulé simple pour SpringApplication n'appelle aucune méthode mais teste simplement l'application de démarrage. [utilise PowerMockRunner.classe]

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.boot.SpringApplication;

@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.management.*"})
public class JobsAppStarterTest {

    @Test
    @PrepareForTest(SpringApplication.class)
    public void testSpringStartUp() {
        PowerMockito.mockStatic(SpringApplication.class);
        SpringApplication.run(JobsAppStarter.class, new String[] {"args"});
        JobsAppStarter.main(new String[] {"args"});
    }
}
 1
Author: Ramji Sridaran, 2020-10-26 06:57:24

, j'ai résolu d'une manière différente ici. Puisque cette méthode n'est là que comme un pont vers l'exécution de Spring, j'ai annoté la méthode avec @lombok.Generated et maintenant sonar l'ignore lors du calcul de la couverture de test.

D'autres annotations @Generated, comme javax.annotation.processing.Generated ou javax.annotation.Generated pourraient également fonctionner mais je ne peux pas tester maintenant car mon ticket de problème a été fermé.

package com.stackoverflow;

import lombok.Generated;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    @Generated
    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }

}
 0
Author: José Roberto Araújo Júnior, 2020-05-21 22:57:41

En plus des réponses ci-dessus, voici un test unitaire de la méthode principale d'une application SpringBoot pour si vous utilisez JUnit 5:

try (MockedStatic<SpringApplication> mocked = mockStatic(SpringApplication.class)) {
            
   mocked.when(() -> { SpringApplication.run(ElectronicGiftCardServiceApplication.class, 
      new String[] { "foo", "bar" }); })
         .thenReturn(Mockito.mock(ConfigurableApplicationContext.class));
            
   ElectronicGiftCardServiceApplication.main(new String[] { "foo", "bar" });
            
   mocked.verify(() -> { SpringApplication.run(ElectronicGiftCardServiceApplication.class, 
      new String[] { "foo", "bar" }); });

}   

Il vérifie que la méthode statique run() sur la classe SpringApplication est appelée avec le tableau de chaînes attendu lorsque nous appelons ElectronicGiftCardServiceApplication.principal().

Même idée que awgtek et Ramji Sridaran, mais leurs solutions sont pour JUnit 4.

 0
Author: Valentijn, 2020-11-30 17:06:05