Maison > Article > développement back-end > Les moqueries, qu'est-ce que c'est ?
Ce texte est le premier d'une série de textes sur les tests dans les applications de traitement de données que j'apporterai ici et sur mon blog personnel.
Lorsque j'ai fait ma transition de carrière d'ingénieur logiciel à ingénieur de données, j'ai commencé à avoir des conversations avec des personnes dans le domaine des données qui n'avaient pas de formation en génie logiciel. Dans ces conversations, une question revenait à plusieurs reprises : comment rédiger des tests ?
Écrire des tests peut, en effet, paraître une tâche complexe pour ceux qui n'y sont pas habitués, car elle nécessite un changement dans la manière d'écrire du code. La vérité est qu’il n’y a pas de mystère, mais plutôt une question de pratique et de répétition. Mon objectif principal dans cet article est de vous guider, qui débutez, dans un processus qui montre comment nous pouvons créer des tests pour les applications qui traitent des données, garantissant la qualité et la fiabilité du code.
Ce texte fait partie d'une série que j'apporterai au cours des prochaines semaines où je partagerai comment écrire des tests automatisés en code destinés à l'ingénierie des données. Dans l'article d'aujourd'hui, je souhaite explorer un peu les simulations. Dans plusieurs scénarios de code, un pipeline de données établira des connexions, des appels d'API, une intégration avec des services Cloud, etc., ce qui peut créer une certaine confusion sur la façon dont nous pouvons tester cette application. Aujourd'hui, nous allons explorer quelques bibliothèques intéressantes pour écrire des tests axés sur l'utilisation de mocks.
Les simulacres sont des objets simulés utilisés dans les tests pour imiter le comportement de dépendances ou de composants externes qui ne sont pas au centre du test. Ils vous permettent d'isoler l'unité de code testée, garantissant ainsi que les tests sont plus contrôlables et prédictifs. L'utilisation de simulations est une pratique courante dans les tests unitaires et les tests d'intégration.
Et nous devrions utiliser des simulations quand :
Dans les pipelines de données, Mocking permet de créer des représentations de composants externes – comme une base de données, un service de messagerie ou une API – sans dépendre de leurs infrastructures réelles. Ceci est particulièrement utile dans les environnements de traitement de données, qui intègrent plusieurs technologies, telles que PySpark pour le traitement distribué, Kafka pour la messagerie, ainsi que des services cloud tels qu'AWS et GCP.
Dans ces scénarios où nous disposons de pipelines de données, Mocking facilite l'exécution de tests isolés et rapides, minimisant les coûts et le temps d'exécution. Il permet de vérifier avec précision chaque partie du pipeline, sans pannes intermittentes causées par des connexions réelles ou une infrastructure externe, et avec la certitude que chaque intégration fonctionne comme prévu.
Dans chaque langage de programmation, on peut trouver des modules internes qui fournissent déjà des fonctions Mock à implémenter. En Python, la bibliothèque native unittest.mock est le principal outil de création de simulations, vous permettant de simuler des objets et des fonctions avec facilité et contrôle. Dans Go, le processus Mocking est généralement pris en charge par des packages externes, tels que Mockery, car le langage ne dispose pas de bibliothèque Mock native ; mockery est particulièrement utile pour générer des mocks à partir d'interfaces, une fonctionnalité native de Go. En Java, Mockito se distingue comme une bibliothèque populaire et puissante pour créer des mocks, s'intégrant à JUnit pour faciliter des tests unitaires robustes. Ces bibliothèques constituent une base essentielle pour tester des composants isolés, en particulier dans les pipelines de données et les systèmes distribués où la simulation de sources de données externes et d'API est essentielle.
Commençons par un exemple de base de la façon dont nous pouvons utiliser les Mocks. Supposons que nous ayons une fonction qui effectue des appels API et que nous devions écrire des tests unitaires pour cette fonction :
def get_data_from_api(url): import requests response = requests.get(url) if response.status_code == 200: return response.json() else: return None
Pour aborder correctement les scénarios de test, nous devons d'abord comprendre quelles situations doivent être couvertes. Comme notre fonction effectue des appels REST, les tests doivent considérer au moins deux scénarios principaux : un dans lequel la requête réussit et un autre dans lequel la réponse n'est pas celle attendue. Nous pourrions exécuter le code avec une vraie URL pour observer le comportement, mais cette approche présente des inconvénients, car nous n'aurions pas de contrôle sur les différents types de réponses, en plus de laisser le test vulnérable aux changements dans la réponse URL ou à son éventuelle indisponibilité. . Pour éviter ces incohérences, nous utiliserons des Mocks.
from unittest import mock @mock.patch('requests.get') def test_get_data_from_api_success(mock_get): # Configura o mock para retornar uma resposta simulada mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"key": "value"} # Chama a função com o mock ativo result = get_data_from_api("http://fakeurl.com") # Verifica se o mock foi chamado corretamente e o resultado é o esperado mock_get.assert_called_once_with("http://fakeurl.com") self.assertEqual(result, {"key": "value"})
Avec la décoration @mock.patch de la bibliothèque Python unittest, nous pouvons remplacer l'appel request.get par un mock, un "faux objet" qui simule le comportement de la fonction get dans le contexte de test, éliminant la dépendance externe .
En définissant des valeurs pour la return_value du mock, nous pouvons spécifier exactement ce que nous attendons que l'objet renvoie lorsqu'il est appelé dans la fonction que nous testons. Il est important que la structure return_value suive la même chose que les objets réels que nous remplaçons. Par exemple, un objet de réponse du module de requêtes a des attributs comme status_code et des méthodes comme json(). Ainsi, pour simuler une réponse de la fonction request.get, on peut attribuer la valeur attendue à ces attributs et méthodes directement dans le mock.
def get_data_from_api(url): import requests response = requests.get(url) if response.status_code == 200: return response.json() else: return None
Dans ce cas précis, l'objectif est de simuler la réponse à la requête, c'est-à-dire de tester le comportement de la fonction avec différents résultats attendus sans dépendre d'une URL externe et sans impact sur notre environnement de test.
from unittest import mock @mock.patch('requests.get') def test_get_data_from_api_success(mock_get): # Configura o mock para retornar uma resposta simulada mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"key": "value"} # Chama a função com o mock ativo result = get_data_from_api("http://fakeurl.com") # Verifica se o mock foi chamado corretamente e o resultado é o esperado mock_get.assert_called_once_with("http://fakeurl.com") self.assertEqual(result, {"key": "value"})
En simulant les réponses d'erreur de l'API dans les tests, nous pouvons aller au-delà des bases et vérifier le comportement de l'application par rapport à différents types de codes d'état HTTP tels que 404, 401, 500 et 503. Cela offre une couverture plus large et garantit que l'application traite de manière appropriée à chaque type de panne, je comprends comment ces variations dans l'appel peuvent impacter notre application/traitement des données. Dans les appels de méthode POST, nous pouvons ajouter une couche supplémentaire de validation, vérifiant non seulement le status_code et le fonctionnement de base de l'appel, mais également le schéma de la réponse envoyée et reçue, garantissant que les données renvoyées suivent le format attendu. Cette approche de test plus détaillée permet d'éviter de futurs problèmes en garantissant que l'application est prête à gérer une variété de scénarios d'erreur et que les données reçues sont toujours conformes à ce qui a été conçu.
Maintenant que nous avons vu un cas simple d'utilisation de Mocks dans du code Python pur, étendons nos cas à un extrait de code qui utilise Pyspark.
Pour tester les fonctions PySpark, en particulier les opérations DataFrame telles que filter, groupBy et join, l'utilisation de simulations est une approche efficace qui élimine le besoin d'exécuter un vrai Spark, réduisant ainsi le temps de test et simplifiant l'environnement de développement. La bibliothèque unittest.mock de Python vous permet de simuler les comportements de ces méthodes, permettant ainsi de vérifier le flux de code et la logique sans dépendre de l'infrastructure Spark.
Voyons, étant donné la fonction suivante, où nous avons une transformation qui effectue des opérations de filtrage, groupBy et de jointure sur les dataframes dans Spark.
def get_data_from_api(url): import requests response = requests.get(url) if response.status_code == 200: return response.json() else: return None
Pour exécuter un test PySpark, nous avons besoin que la configuration de Spark soit effectuée localement. Cette configuration se fait dans la méthode setUpClass, qui crée une instance de Spark qui sera utilisée dans tous les tests de la classe. Cela nous permet d'exécuter PySpark de manière isolée, ce qui permet d'effectuer de véritables opérations de transformation sans recourir à un cluster complet. Une fois le test terminé, la méthode tearDownClass est chargée de mettre fin à la session Spark, en garantissant que toutes les ressources sont correctement libérées et que l'environnement de test est propre.
from unittest import mock @mock.patch('requests.get') def test_get_data_from_api_success(mock_get): # Configura o mock para retornar uma resposta simulada mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"key": "value"} # Chama a função com o mock ativo result = get_data_from_api("http://fakeurl.com") # Verifica se o mock foi chamado corretamente e o resultado é o esperado mock_get.assert_called_once_with("http://fakeurl.com") self.assertEqual(result, {"key": "value"})
Dans le test test_transform_data, nous commençons par créer des exemples de DataFrames pour df et df_other, qui contiennent les données qui seront utilisées dans les transformations. Nous exécutons ensuite la fonction transform_data sans appliquer de simulations, permettant aux opérations de filtrage, groupBy et de jointure de se produire réellement et d'aboutir à un nouveau DataFrame. Après exécution, nous utilisons la méthode collect() pour extraire les données du DataFrame résultant, ce qui nous permet de comparer ces données avec les valeurs attendues et, ainsi, de valider la transformation effectuée de manière réelle et précise.
Mais nous pouvons aussi avoir des scénarios dans lesquels nous souhaitons tester le résultat d'une de ces fonctions pyspark. Il est nécessaire de moquer une autre partie du code qui peut représenter un goulot d'étranglement au moment de l'exécution et qui ne représente pas de risque pour notre processus. On peut donc utiliser la technique de moquerie d'une fonction/module, comme nous l'avons vu dans l'exemple précédent en utilisant des requêtes.
response.status_code = mock_get.return_value.status_code response.json() = mock_get.return_value.json.return_value
Le test Mock pour une opération spécifique a été effectué dans la méthode test_transform_data_with_mocked_join, où nous avons appliqué une simulation spécifiquement pour la méthode de filtrage. Cette simulation remplace le résultat de l'opération de jointure par un DataFrame simulé, permettant aux opérations précédentes, telles que groupBy et join, d'être exécutées de manière réelle. Le test compare ensuite le DataFrame résultant avec la valeur attendue, garantissant que la simulation de jointure a été utilisée correctement, sans interférer avec les autres transformations effectuées.
Cette approche hybride apporte plusieurs avantages. En garantissant que les opérations PySpark réelles telles que join et groupBy sont maintenues, nous pouvons valider la logique des transformations sans perdre la flexibilité de remplacer des opérations spécifiques telles que filter par des simulations. Cela se traduit par des tests plus robustes et plus rapides, éliminant le besoin d'un cluster Spark complet, ce qui facilite le développement et la validation continus du code.
Il est important de souligner que cette stratégie doit être utilisée avec prudence et uniquement dans des scénarios où aucun biais dans les résultats n'est créé. Le but du test est de garantir que le traitement se déroule correctement ; Nous ne devrions pas simplement attribuer des valeurs sans réellement tester la fonction. Bien qu'il soit valable de simuler des sections dont nous pouvons garantir qu'elles n'affecteront pas le processus de tests unitaires, il est essentiel de se rappeler que la fonction doit être exécutée pour valider son comportement réel.
Ainsi, l’approche hybride prend beaucoup plus de sens lorsque l’on ajoute d’autres types de traitements à cette fonction. Cette stratégie permet une combinaison efficace d'opérations réelles et simulées, garantissant des tests plus robustes et fiables
Les simulations sont de précieux alliés pour créer des tests efficaces, notamment lorsqu'il s'agit de travailler avec PySpark et d'autres services cloud. L'implémentation que nous avons explorée à l'aide de unittest en Python nous a non seulement aidé à simuler des opérations, mais également à maintenir l'intégrité de nos données et de nos processus. Grâce à la flexibilité offerte par les simulations, nous pouvons tester nos pipelines sans craindre de faire des ravages dans les environnements de production. Alors, prêt pour le prochain défi ? Dans notre prochain texte, nous plongerons dans le monde des intégrations avec les services AWS et GCP, montrant comment simuler ces appels et garantir que vos pipelines fonctionnent parfaitement. À la prochaine fois !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!