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.
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?
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[] {});
}
}
Vous pouvez faire quelque chose comme ça
@Test
public void applicationContextLoaded() {
}
@Test
public void applicationContextTest() {
mainApp.main(new String[] {});
}
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.
<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.
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"});
}
}
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));
}
}
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"});
}
}
, 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);
}
}
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.