Cet article résume les réflexions sur la conception d'un environnement de test unitaire facile à implémenter et à maintenir dans une application Web qui utilise Spring Boot. Puisqu'il s'agit d'un article de conception, il n'aborde pas la façon d'écrire concrètement du code de test.
environnement
référence
Dans cette application de démonstration, les trois sous-packages suivants sont créés pour chaque couche. La division des couches en sous-packages facilite la création d'un environnement de test pour chaque couche.
src.main.java
|
+--- com.example.demo //★package root
| |
| +--- Application.java //★Main Application Class
| |
| +--- domain //★ Couche de domaine
| | |
| | +--- DatasourceConfig.java //★ Configuration de la source de données
| | |
| | +--- entity //☆ Placez la classe d'entité JPA
| | |
| | +--- repository //☆ Placer l'interface du référentiel JPA
| | | (spring-data-jpa)
| | +--- service //☆ Placez la classe de logique métier
| | |
| | +--- impl
| |
| +--- external //★ Couche externe
| | |
| | +--- service //☆ Placez la classe de logique métier
| | |
| | +--- impl
| |
| +--- web //★ couche Web
| |
| +--- WebMvcConfig.java //★ Configuration WebMvc
| +--- JacksonConfig.java //★ Configuration de Jackson
| |
| +--- advice
| | |
| | +--- CustomControllerAdvice.java
| |
| +--- interceptor
| | |
| | +--- CustomHandlerInterceptor.java
| |
| +--- controller //☆ Classe de contrôleur de place
|
src.java.resources
|
+--- application.yml
src.test.java
|
+--- com.example.demo
| |
| +--- domain
| | |
| | +--- DomainTestApplication.java //★ Classe d'application principale pour le test de la couche de domaine
| | |
| | +--- entity
| | |
| | +--- repository
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- external
| | |
| | +--- ExternalTestApplication.java //★ Classe d'application principale pour les tests de couche externe
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- web
| |
| +--- WebTestApplication.java //★ Classe d'application principale de test de niveau Web
| |
| +--- controller
|
src.test.resources
|
+--- application.yml
Dans Spring Boot, il est recommandé de placer la classe d'application principale avec l'annotation d'application Spring Boot dans la racine du package (ou un package supérieur aux autres classes). Placer cette classe au-dessus des autres classes analysera automatiquement le composant subordonné (avec les annotations Component et Service) et les classes de configuration.
14.2 Locating the Main Application Class We generally recommend that you locate your main application class in a root package above other classes
Cette classe est un contenu général (commun), il n'y a donc rien de spécial à mentionner.
Application
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
La classe de configuration liée à la couche de domaine est placée directement sous le package de domaine. Cet exemple est une classe qui configure la source de données. Je ne l'ai pas configuré dans ce code, mais je suppose que c'est dans cette classe si vous devez personnaliser la source de données ou le gestionnaire de transactions.
DatasourceConfig
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EntityScan
public class DatasourceConfig {
// nothing
}
En outre, bien que omis dans le code ci-dessus, vous pouvez spécifier les packages à analyser dans les basesPackages comme indiqué ci-dessous.
@EnableJpaRepositories(basePackages = {"com.example.demo.domain.repository"})
@EntityScan(basePackages = {"com.example.demo.domain.entity"})
La classe de configuration liée à la couche Web est placée directement sous le package Web. Cet exemple montre une classe qui personnalise WebMvc et
WebMvcConfig
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustomHandlerInterceptor())
.addPathPatterns("/memo/**");
}
}
Ceci est une classe pour personnaliser Jackson. Le code ci-dessous personnalise l'ObjectMapper, mais vous pouvez faire de même avec le fichier de configuration.
JacksonConfig
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.serializationInclusion(JsonInclude.Include.NON_NULL)
.indentOutput(true)
.failOnUnknownProperties(false)
.failOnEmptyBeans(false)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToEnable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS);
return builder;
}
}
Préparez des classes de point d'entrée de code de test pour chaque couche. En préparant chaque couche, vous ne pouvez capturer que les dépendances requises.
La racine du package pour les tests de niveau domaine est com.example.demo.domain et la classe DomainTestApplication est le point d'entrée pour les tests.
src.main.java
|
+--- com.example.demo
| |
| +--- domain
| |
| +--- DatasourceConfig.java //★ Configuration de la source de données
| |
| +--- entity
| |
| +--- repository
| |
| +--- service
| |
| +--- impl
|
src.java.resources
|
+--- application.yml
src.test.java
|
+--- com.example.demo
| |
| +--- domain
| |
| +--- DomainTestApplication.java //★ Classe d'application principale pour le test de la couche de domaine
| |
| +--- entity
| |
| +--- repository
| |
| +--- service
| |
| +--- impl
|
src.test.resources
|
+--- application.yml
DomainTestApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DomainTestApplication {
public static void main(String[] args) {
SpringApplication.run(DomainTestApplication.class, args);
}
}
[DataJpaTest] pour le test unitaire du référentiel (https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.html) Utilisez des annotations. Si vous ajoutez l'annotation DataJpaTest, la base de données intégrée sera utilisée quels que soient les paramètres de la source de données. (H2 est utilisé dans cette application de démonstration) De plus, au lieu d'EntityManager, TestEntityManager Est disponible.
@RunWith(SpringRunner.class)
@DataJpaTest
public class MemoRepositoryTests {
@Autowired
private TestEntityManager testEntityManager;
@Autowired
private MemoRepository memoRepository;
//Code de test
}
Si vous souhaitez effectuer le test d'intégration à l'aide de la source de données définie dans le fichier de configuration au lieu de la base de données intégrée, [AutoConfigureTestDatabase](https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/ org / springframework / boot / test / autoconfigure / jdbc / AutoConfigureTestDatabase.html) Vous pouvez modifier le paramètre avec l'annotation.
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemoRepositoryIntegrationTests {
@Autowired
private TestEntityManager testEntityManager;
@Autowired
private MemoRepository memoRepository;
//Code de test
}
Si vous souhaitez utiliser un fichier sql ou une instruction sql pour saisir des données de test, [Sql](https://docs.spring.io/spring-framework/docs/5.0.4.RELEASE/javadoc-api/org/springframework/test /context/jdbc/Sql.html) Les annotations sont disponibles. Les annotations peuvent être attachées aux classes et aux méthodes, mais si elles sont attachées aux deux comme décrit dans JavaDoc, les paramètres de méthode sont prioritaires.
Method-level declarations override class-level declarations.
@Sql(statements = {
"INSERT INTO memo (id, title, description, done, updated) VALUES (11, 'test title 1', 'test description', false, CURRENT_TIMESTAMP)",
"INSERT INTO memo (id, title, description, done, updated) VALUES (12, 'test title 2', 'test description', true, CURRENT_TIMESTAMP)",
"INSERT INTO memo (id, title, description, done, updated) VALUES (13, 'test title 3', 'test description', false, CURRENT_TIMESTAMP)",
})
AutoConfigureTestEntityManager Utilisez l'annotation Si tel est le cas, TestEntityManager peut être utilisé sans l'annotation DataJpaTest.
Spring Runner n'est pas requis pour les tests unitaires des services car il ne dépend pas du Spring Framework. Les composants dont dépend la cible de test sont simulés (ou espionnés) dans Mockito.
public class MemoServiceImplTests {
@Rule
public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
@Mock
private MemoRepository memoRepository;
@InjectMocks
private MemoServiceImpl sut;
//Code de test
}
Utilisez l'annotation SpringBootTest pour les tests d'intégration de services Faire. De plus, étant donné que la fonction de serveur Web n'est pas nécessaire, WebEnvironment.NONE est spécifié pour webEnvironment. S'il est défini sur NONE, le serveur Web intégré ne démarrera pas. La source de données du fichier de configuration est utilisée pour accéder à la base de données. Vous pouvez utiliser l'annotation Sql mentionnée ci-dessus ou EntityManager avec le code ci-dessous pour saisir les données de test. Autres méthodes décrites dans 80. Database Initialization Il y a.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Transactional
public class MemoServiceImplIntegrationTests {
@Autowired
private EntityManager entityManager;
@Autowired
private MemoServiceImpl sut;
//Code de test
}
La racine du package pour les tests de niveau externe est com.example.demo.external et la classe ExternalTestApplication est le point d'entrée pour les tests.
src.main.java
|
+--- com.example.demo
| |
| +--- external
| |
| +--- service
| |
| +--- impl
|
src.java.resources
|
+--- application.yml
src.test.java
|
+--- com.example.demo
| |
| +--- external
| |
| +--- ExternalTestApplication.java //★ Classe d'application principale pour les tests de couche externe
| |
| +--- service
| |
| +--- impl
|
src.test.resources
|
+--- application.yml
Excluez DataSourceAutoConfiguration de l'AutoConfiguration car le niveau externe est indépendant de la base de données.
ExternalTestApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class
})
public class ExternalTestApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalTestApplication.class, args);
}
}
Puisqu'il s'agit d'une classe de test similaire au test du service de la couche domaine, elle est omise.
La racine du package pour les tests de niveau Web est com.example.demo.web et la classe WebTestApplication est le point d'entrée pour les tests.
src.main.java
|
+--- com.example.demo
| |
| +--- domain
| | |
| | +--- DatasourceConfig.java //★ Configuration de la source de données
| | |
| | +--- entity
| | |
| | +--- repository
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- external
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- web
| |
| +--- WebMvcConfig.java //★ Configuration WebMvc
| +--- JacksonConfig.java //★ Configuration de Jackson
| |
| +--- advice
| | |
| | +--- MyControllerAdvice.java
| |
| +--- interceptor
| | |
| | +--- MyHandlerInterceptor.java
| |
| +--- controller
|
src.java.resources
|
+--- application.yml
src.test.java
|
+--- com.example.demo
| |
| +--- domain
| | |
| | +--- entity
| | |
| | +--- repository
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- external
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- web
| |
| +--- WebTestApplication.java //★ Classe d'application principale de test de niveau Web
| |
| +--- controller
|
src.test.resources
|
+--- application.yml
Comme la couche Web dépend de la couche de domaine et de la couche externe, le package cible est spécifié dans scanBasePackages.
WebTestApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {
"com.example.demo.domain.service",
"com.example.demo.external.service",
"com.example.demo.web"
})
public class WebTestApplication {
public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}
}
Dans le test unitaire de Json, ajoutez l'annotation JsonTest. Je vais l'utiliser. Il s'agit d'un test unitaire pour voir si le résultat de la conversion d'une entité en json est sérialisé comme prévu. Testez les boucles infinies si vous personnalisez la sérialisation avec les annotations JsonProperty ou JsonIgnore de Jackson, ou s'il existe des références mutuelles entre entités, en particulier avec les annotations liées à JPA (OneToMany, ManyToOne, etc.).
@RunWith(SpringRunner.class)
@JsonTest
public class MemoToJsonTests {
@Autowired
private JacksonTester<Memo> json;
//Code de test
}
WebMvcTest Utilisez des annotations. WebMvcTest peut utiliser MockMvc et WebClient (nécessite HtmlUnit pour la dépendance).
Le [MockBean] de Spring Boot (https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test) se moque des composants dont dépend le contrôleur testé /mock/mockito/MockBean.html) (Bean espion [SpyBean](https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test/mock /mockito/SpyBean.html)) Utilisez des annotations. Les objets simulés par MockBean sont ajoutés au contexte de l'application et injectés dans la cible de test (MemoController dans cet exemple).
@RunWith(SpringRunner.class)
@WebMvcTest(MemoController.class)
public class MemoControllerTests {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private MemoService memoService;
//Code de test
}
AutoConfigureMockMvc Utilisez l'annotation Si vous le faites, vous pourrez utiliser MockMvc sans l'annotation WebMvcTest.
Dans les classes de test qui utilisent l'annotation SpringBootTest, au lieu de RestTemplate, [TestRestTemplate](https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test/web /client/TestRestTemplate.html) est disponible. Importez la classe DatasourceConfig car le test d'intégration du contrôleur nécessite un accès à la base de données.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(value = {DatasourceConfig.class})
public class MemoControllerIntegrationTests {
@Autowired
private TestRestTemplate testRestTemplate;
//Code de test
}
En définissant une classe de paramètres comme le code ci-dessous, vous pouvez remplacer la source de données par la base de données intégrée dans n'importe quel test d'intégration.
@TestConfiguration
public class WebTestConfig {
@Bean
public DataSource datasource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.setScriptEncoding("UTF-8")
.addScripts("classpath:scripts/init.sql")
.build();
}
}
TestConfiguration La classe de configuration utilisant des annotations est SpringBootConfiguration. Puisqu'il n'est pas soumis à la détection automatique des annotations, il doit être importé manuellement.
@Import(value = {DatasourceConfig.class, WebTestConfig.class})
En passant, lors de la création d'un schéma ou de la saisie de données de test, il est difficile à utiliser car il doit être écrit en SQL qui peut être utilisé même dans une base de données intégrée.
Si vous définissez la propriété de débogage dans le fichier de paramètres, le journal de débogage sera généré. (La même chose s'applique si vous spécifiez -Ddebug
dans les propriétés système)
debug: true
Le résultat de la configuration automatique "RAPPORT D'ÉVALUATION DES CONDITIONS" est émis dans le journal de débogage, vous pouvez donc vérifier l'état de la configuration.
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
AopAutoConfiguration matched:
- @ConditionalOnClass found required classes 'org.springframework.context.annotation.EnableAspectJAutoProxy', 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice', 'org.aspectj.weaver.AnnotatedElement'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)
//réduction
Negative matches:
-----------------
ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)
//réduction
Exclusions:
-----------
None
Unconditional classes:
----------------------
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
//réduction
Dans les tests unitaires avec Mockito 1.x, j'ai parfois vu du code de test qui remplaçait le champ privé sous test par une classe appelée Whitebox, mais depuis Mockito 2.1, cette classe n'est plus disponible.
Whitebox.setInternalState(sut, "someService", mockSomeService);
Dans Spring (Boot), l'annotation Value peut injecter la valeur de réglage dans le champ privé, je l'ai donc vue également utilisée dans de tels cas.
@Value("${app.someValue}")
private String someValue;
La raison pour laquelle l'équipe de développement de Mockito a supprimé Whitebox peut être trouvée dans les problèmes suivants, mais dans une traduction approximative, «l'utilisation facile de Whitebox a stimulé la production de masse de code de test de mauvaise qualité. Il semble que ce soit de.
Au printemps (Boot), au lieu de Whitebox, ReflectionTestUtils Vous pouvez utiliser des classes, mais si le remplacement basé sur la réflexion est mauvais, vous hésitez également à utiliser cette méthode.
Une autre alternative consiste à changer la visibilité du champ de privé à package privé. La classe de test étant généralement dans le même package que la classe testée, il est possible de réécrire les champs directement à partir du code de test. Il n'est pas rare d'étendre la visibilité pour faciliter les tests, par exemple VisibleForTesting pour Google Guava. Il existe une annotation (/google/common/annotations/VisibleForTesting.html). Étant donné que cette annotation est une annotation de marqueur, elle n'étend pas automatiquement la plage visible pendant le test.
Je ne suis pas familier avec le développement piloté par les tests et les techniques de test unitaire, donc je ne peux pas commenter le fait que l'utilisation de la réflexion est "mauvaise" et que la visibilité est "meilleure que".
Recommended Posts