Contrairement à de nombreuses plates-formes, Java souffre d'un manque de bibliothèques de stub de connexion. Si vous êtes dans ce monde depuis longtemps, vous devriez probablement être familier avec WireMock, Betamax ou même Spock. De nombreux développeurs dans les tests utilisent Mockito pour décrire le comportement des objets, DataJpaTest avec une base de données h2 locale, tests Cucumber. Aujourd'hui, vous rencontrerez une alternative légère qui vous aidera à faire face aux différents problèmes que vous pourriez rencontrer en utilisant ces approches. En particulier, anyStub essaie de résoudre les problèmes suivants:
- simplifier la configuration de l'environnement de test
- automatiser la collecte de données pour les tests
- continuez à tester votre application et évitez de tester autre chose
Qu'est-ce qu'anyStub et comment ça marche
AnyStub encapsule les appels de fonction, essayant de trouver les appels correspondants qui ont déjà été enregistrés. Deux choses peuvent se produire avec ceci:
- s'il y a un appel correspondant, anyStub restaure le résultat enregistré associé à cet appel et le renvoie
- s'il n'y a pas d'appel correspondant et que l'accès au système externe est autorisé, anyStub fera cet appel, enregistrera ce résultat et le renverra
Dès la sortie de la boîte, anyStub fournit des wrappers pour le client http d'Apache HttpClient pour créer des stubs pour les requêtes http et plusieurs interfaces à partir de javax.sql. * Pour les connexions DB. Vous disposez également d'une API pour créer des stubs pour d'autres connexions.
AnyStub est une bibliothèque de classes simple et ne nécessite pas de configuration spéciale de votre environnement. Cette bibliothèque est destinée à travailler avec des applications à démarrage par ressort et vous obtiendrez le maximum d'avantages en suivant ce chemin. Vous pouvez l'utiliser en dehors de Spring, dans des applications Java simples, mais vous devrez certainement faire un travail supplémentaire. La description suivante se concentre sur le test des applications Spring-Boot.
Regardons les tests d'intégration. Il s'agit de la manière la plus excitante et la plus complète de tester votre système. En fait, Spring-boot et JUnit font presque tout pour vous lorsque vous écrivez des annotations magiques:
@RunWith(SpringRunner.class) @SpringBootTest
À l'heure actuelle, les tests d'intégration sont sous-estimés et sont utilisés dans une mesure limitée, et certains développeurs les évitent. Cela est principalement dû à la préparation et à la maintenance fastidieuses des tests ou à la nécessité d'une configuration spéciale de l'environnement sur les serveurs de build.
Avec anyStub, vous n'avez pas à paralyser le contexte printanier. Au lieu de cela, garder le contexte proche de la configuration de production est simple et direct.
Dans cet exemple, nous verrons comment connecter anyStub à un Consuming a RESTful Web Service à partir du manuel de Pivotal.
Connexion d'une bibliothèque via pom.xml
<dependency> <groupId>org.anystub</groupId> <artifactId>anystub</artifactId> <version>0.2.27</version> <scope>test</scope> </dependency>
L'étape suivante consiste à modifier le contexte du ressort.
package hello; import org.anystub.http.StubHttpClient; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class TestConfiguration { @Bean public RestTemplateBuilder builder() { RestTemplateCustomizer restTemplateCustomizer = new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { HttpClient real = HttpClientBuilder.create().build(); StubHttpClient stubHttpClient = new StubHttpClient(real); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(stubHttpClient); restTemplate.setRequestFactory(requestFactory); } }; return new RestTemplateBuilder(restTemplateCustomizer); } }
Cette modification ne modifie pas les relations de composants dans l'application, mais remplace uniquement l'implémentation d'une interface unique. Cela nous renvoie au principe de substitution de Barbara Lisk . Si la conception de votre application ne la viole pas, cette substitution ne violera pas la fonctionnalité.
Tout est prêt. Ce projet comprend déjà un test.
@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Autowired private RestTemplate restTemplate; @Test public void contextLoads() { assertThat(restTemplate).isNotNull(); } }
Ce test est vide, mais il exécute déjà le contexte de l'application. Le plaisir commence ici . Comme nous l'avons dit ci-dessus, le contexte d'application dans le test coïncide avec le contexte de travail dans lequel le CommandLineRunner est créé dans lequel la requête http au système externe est exécutée.
@SpringBootApplication public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public static void main(String args[]) { SpringApplication.run(Application.class); } @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } @Bean public CommandLineRunner run(RestTemplate restTemplate) throws Exception { return args -> { Quote quote = restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); log.info(quote.toString()); }; } }
Cela suffit pour démontrer le fonctionnement de la bibliothèque. Après avoir démarré les tests pour la première fois, vous trouverez le nouveau complete/src/test/resources/anystub/stub.yml
.
request0: exception: [] keys: [GET, HTTP/1.1, 'https://gturnquist-quoters.cfapps.io/api/random'] values: [HTTP/1.1, '200', OK, 'Content-Type: application/json;charset=UTF-8', 'Date: Thu, 25 Apr 2019 23:04:49 GMT', 'X-Vcap-Request-Id: 5ffce9f3-d972-4e95-6b5c-f88f9b0ae29b', 'Content-Length: 177', 'Connection: keep-alive', '{"type":"success","value":{"id":3,"quote":"Spring has come quite a ways in addressing developer enjoyment and ease of use since the last time I built an application using it."}}']
Qu'est-il arrivé? spring-boot a intégré RestTemplateBuilder à partir de la configuration de test dans l'application. Cela a conduit l'application à travailler sur l'implémentation du stub du client http. StubHttpClient a intercepté la demande, n'a pas trouvé le fichier de raccord, a exécuté la demande, a enregistré le résultat dans un fichier et a renvoyé le résultat récupéré à partir du fichier.
A partir de maintenant, vous pouvez exécuter ce test sans connexion Internet et cette demande sera réussie. restTemplate.getForObject()
renverra le même résultat. Vous pouvez compter sur ce fait dans vos futurs tests.
Vous pouvez trouver toutes les modifications décrites sur GitHub .
En fait, nous n'avons toujours pas créé un seul test. Avant d'écrire des tests, voyons comment cela fonctionne avec les bases de données.
Dans cet exemple, nous allons ajouter un test d'intégration à Accès aux données relationnelles à l'aide de JDBC avec Spring à partir du didacticiel Pivotal.
La configuration de test pour ce cas ressemble à ceci:
package hello; import org.anystub.jdbc.StubDataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class TestConfiguration { @Bean public DataSource dataSource() { JdbcDataSource ds = new JdbcDataSource(); ds.setURL("jdbc:h2:./test"); return new StubDataSource(ds); } }
Ici, une source de données régulière vers une base de données externe est créée et encapsulée avec une implémentation de stub - la classe StubDataSource. Spring-boot l'intègre dans son contexte. Nous devons également créer au moins un test pour exécuter le contexte Spring dans le test.
package hello; import org.anystub.AnyStubId; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Test @AnyStubId public void test() { } }
Il s'agit là encore d'un test vide - sa seule tâche est d'exécuter le contexte de l'application. Nous voyons ici une annotation très importante @AnystubId
, mais elle ne sera pas encore impliquée.
Après la première exécution, vous trouverez un nouveau src/test/resources/anystub/stub.yml
qui inclut tous les appels de base de données. Vous serez surpris de voir comment le printemps fonctionne en arrière-plan avec les bases de données. Notez que de nouvelles exécutions du test n'aboutiront pas à un véritable accès à la base de données. Si vous supprimez test.mv.db, il n'apparaîtra pas après des exécutions répétées des tests. L'ensemble complet des modifications peut être consulté sur GitHub .
Pour résumer. avec anyStub:
- vous n'avez pas besoin de configurer spécifiquement un environnement de test
- les tests sont effectués avec des données réelles
- la première exécution des tests prouve vos hypothèses et enregistre les données de test, les suivantes vérifient que le système ne s'est pas dégradé
Vous avez probablement des questions: comment cela couvre-t-il les cas où la base de données n'existe pas encore, que faire avec les tests négatifs et la gestion des exceptions. Nous y reviendrons, mais d'abord, nous traiterons de l'écriture de tests simples.
Nous expérimentons maintenant la consommation d'un service Web RESTful . Ce projet ne contient pas de composants pouvant être testés. Deux classes sont créées ci-dessous, qui devraient représenter deux couches d'une conception d'architecture.
package hello; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class DataProvider { private final RestTemplate restTemplate; public DataProvider(RestTemplate restTemplate) { this.restTemplate = restTemplate; } Quote provideData() { return restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); } }
DataProvider donne accès aux données dans volatile système externe.
package hello; import org.springframework.stereotype.Component; @Component public class DataProcessor { private final DataProvider dataProvider; public DataProcessor(DataProvider dataProvider) { this.dataProvider = dataProvider; } int processData() { return dataProvider.provideData().getValue().getQuote().length(); } }
DataProcessor traitera les données d'un système externe.
Nous avons l'intention de tester le DataProcessor
. Il est nécessaire de tester l'exactitude de l'algorithme de traitement et de protéger le système contre la dégradation des changements futurs.
Pour atteindre ces objectifs, vous pouvez envisager de créer un objet maquette DataProvider avec un ensemble de données et de le transmettre au constructeur DataProcessor dans les tests. Une autre façon pourrait être de décomposer le DataProcessor pour mettre en évidence le traitement de la classe Quote. Ensuite, une telle classe est facile à tester à l'aide de tests unitaires (c'est sûrement la méthode recommandée dans les livres respectés sur le code propre). Essayons d'éviter les changements de code et l'invention des données de test et écrivons simplement un test.
@RunWith(SpringRunner.class) @SpringBootTest public class DataProcessorTest { @Autowired private DataProcessor dataProcessor; @Test @AnyStubId(filename = "stub") public void processDataTest() { assertEquals(131, dataProcessor.processData()); } }
Il est temps de parler de l'annotation @AnystubId. Cette annotation permet de gérer et de contrôler les fichiers de raccord dans les tests. Il peut être utilisé avec une classe de test ou sa méthode. Cette annotation configure un fichier de raccord individuel pour la zone correspondante. Si une zone est simultanément couverte par des annotations au niveau de la classe et de la méthode, l'annotation de la méthode est prioritaire. Cette annotation a le paramètre filename, qui définit le nom du fichier de raccord. l'extension ".yml" est ajoutée automatiquement si elle est omise. En exécutant ce test, vous ne trouverez pas de nouveau fichier. Le src/test/resources/anystub/stub.yml
a déjà été créé précédemment et ce test le réutilisera. Nous avons obtenu le numéro 131 de ce talon en analysant le résultat de la requête.
@Test @AnyStubId public void processDataTest2() { assertEquals(131, dataProcessor.processData()); Base base = getStub(); assertEquals(1, base.times("GET")); assertTrue(base.history().findFirst().get().matchEx_to(null, null, ".*gturnquist-quoters.cfapps.io.*")); }
Dans ce test, l'annotation @AnyStubId apparaît sans le paramètre de nom de fichier. Dans ce cas, le src/test/resources/anystubprocessDataTest2.yml
. Le nom du fichier est construit à partir du nom de la fonction (classe) + ".yml". Une fois que anyStub crée un nouveau fichier pour ce test, vous devez effectuer un véritable appel système. Et c'est notre chance que le nouveau devis ait la même longueur. Les deux dernières vérifications montrent comment tester le comportement de l'application. Il est à votre disposition: sélection de requêtes par paramètres ou parties de paramètres et comptage du nombre de requêtes. Il existe plusieurs variantes des heures et des fonctions de correspondance qui peuvent être trouvées dans la documentation .
@Test @AnyStubId(requestMode = RequestMode.rmTrack) public void processDataTest3() { assertEquals(79, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); assertEquals(168, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); Base base = getStub(); assertEquals(4, base.times("GET")); }
Dans ce test, @AnyStubId apparaît avec le nouveau paramètre requestMode. Il vous permet de gérer les autorisations pour les fichiers de raccord. Il y a deux aspects à contrôler: la recherche de fichiers et l'autorisation d'appeler un système externe.
RequestMode.rmTrack
définit les règles suivantes: si le fichier vient d'être créé, toutes les demandes sont envoyées au système externe et sont écrites dans le fichier avec les réponses, qu'il y ait ou non une demande identique dans le fichier (les doublons dans le fichier sont autorisés). Si, avant d'exécuter les tests, le fichier de raccord existe, les demandes au système externe sont interdites. Les appels sont attendus exactement dans la même séquence. Si la demande suivante ne correspond pas à la demande du fichier, une exception est levée.
RequestMode.rmNew
ce mode est activé par défaut. Chaque demande est recherchée dans le fichier de raccord. Si une demande correspondante est trouvée - le résultat correspondant est restauré à partir du fichier, la demande au système externe est reportée. Si la demande n'est pas trouvée, le système externe est demandé, le résultat est enregistré dans un fichier. Demandes en double dans le fichier - ne se produisent pas.
RequestMode.rmNone
Chaque demande est recherchée dans un fichier de raccord. Si une requête correspondante est trouvée, son résultat est restauré à partir du fichier. Si le test génère une demande qui n'est pas dans le fichier, une exception est levée.
RequestMode.rmAll
avant la première demande, le fichier de raccord est effacé. Toutes les demandes sont écrites dans le fichier (les doublons dans le fichier sont autorisés). Vous pouvez utiliser ce mode si vous souhaitez regarder le travail de connexion.
RequestMode.rmPassThrough
toutes les demandes sont envoyées directement au système externe, en contournant le talon d'implémentation.
Ces modifications sont disponibles sur GitHub.
Quoi d'autre?
Nous avons vu comment anyStub enregistre les réponses. Si une exception est levée lors de l'accès à un système externe, anyStub l'enregistrera et la lira lors des requêtes suivantes.
Souvent, des exceptions sont levées par les classes de niveau supérieur, tandis que les classes de connexion reçoivent une réponse valide (probablement avec un code d'erreur). Dans ce cas, anyStub est responsable de reproduire la réponse même avec le code d'erreur, et les classes de niveau supérieur lèveront également des exceptions pour vos tests.
Ajoutez des fichiers de raccord au référentiel.
N'ayez pas peur de supprimer et d'écraser des fichiers de raccord.
Gérez judicieusement les fichiers de raccord. Vous pouvez réutiliser un fichier dans plusieurs tests ou fournir un fichier individuel pour chaque test. Profitez de cette opportunité pour vos besoins. Mais généralement, l'utilisation d'un seul fichier avec différents modes d'accès est une mauvaise idée.
Ce sont toutes les principales fonctionnalités de anyStub.