Fermer

mars 13, 2024

Écrire des objets Python testables dans Databricks / Blogs / Perficient

Écrire des objets Python testables dans Databricks / Blogs / Perficient


J’ai écrit sur Développement piloté par les tests dans Databricks et certains des questions intéressantes que vous pouvez rencontrer avec des objets Python. J’ai toujours pensé qu’un code qui n’est pas testable est détestable. Certes, il a été très difficile d’arriver là où je voulais être avec Databricks et TDD. Malheureusement, il est difficile de trouver de la force dans le courage de ses convictions quand on n’a ni l’une ni l’autre. Entrons dans le vif du sujet et discutons honnêtement et en détail de la manière de créer des systèmes Databricks résilients et fiables. Nous verrons comment créer des objets à la fois utilisables et testables. Nous examinerons ensuite les outils de test dont nous disposons. Enfin, nous examinerons quelques directives pratiques pour structurer le code de votre projet Databricks et le déployer en production.

Objets Python testables

Nous avons regardé sous le capot pour voir comment problèmes de décapage peut mener à Py4J problèmes. Nous avons même vu comment cela peut se produire avec du code trivial, comme celui-ci :

class APIError(Exception):
"""Exception raised for errors in the API call."""


class ValidationError(Exception):
"""Exception raised for validation errors."""


class DataError(Exception):
"""Exception raised for errors in data processing."""

Nous savons maintenant qu’il existe un ensemble spécifique de mappages de types qui peut être utilisé entre Python et Java. Logiquement, les objets personnalisés ne figureront jamais dans cet ensemble. Une façon de gérer cela consiste à demander à n’importe quelle méthode qui appelle cette classe de convertir la valeur en chaîne. Il s’agit d’une odeur de code définitive ; SÉCHER spécifiquement. Ce n’est pas trop demander à ces classes anémiques d’aider à centraliser la responsabilité du risque de Py4JErrors. C’est tout ce dont nous parlons.

class APIError(Exception):
    """Exception raised for errors in the API call."""
    def __str__(self):
        return f"API Error: {super().__str__()}"

class DataError(Exception):
    """Exception raised for errors in data processing."""
    def __str__(self):
        return f"Data Error: {super().__str__()}"

class ValidationError(Exception):
    """Exception raised for validation errors."""
    def __str__(self):
        return f"Validation Error: {super().__str__()}"

Si vous regardez ce changement strictement d’un point de vue naïf de mise en œuvre, nous sommes seulement partis de là…

exception_str = str(exception)
if isinstance(exception, APIError): 
    log_content = f"{APP_NAME}: API Error: {exception_str}" 
elif isinstance(exception, ValidationError): 
    log_content = f"{APP_NAME}: Validation Error: {exception_str}" 
elif isinstance(exception, DataError): 
     log_content = f"{APP_NAME}: Data Error: {exception_str}"

… pour ça:

log_content = str(exception)

C’est toujours agréable de réduire les lignes de code, mais LoC est l’une des métriques de code les moins significatives pour une raison. Plus important encore, nous avons désormais supprimé une catégorie d’erreur de notre code. Une autre méthode ne peut pas provoquer de P4JavaError lors de l’utilisation de notre classe. La magie ne réside pas dans la suppression elif; c’est en supprimant except Py4JJavaError as err:. Il s’agit d’un concept fondamental dans la création d’un code plus testable ; supprimant la possibilité d’exceptions.

Simulations de Databricks

Intelligence des données - L'avenir du Big Data
L’avenir du Big Data

Avec quelques conseils, vous pouvez créer une plateforme de données adaptée aux besoins de votre organisation et tirer le meilleur parti de votre capital de données.

Obtenez le guide

Il est difficile d’imaginer de bons tests unitaires sans simulations. Il est également difficile d’imaginer des moqueries qui m’ont rendu la vie plus difficile que de se moquer de Databricks. Pour en revenir au cas de la journalisation, j’avais une méthode très simple qui prenait cela log_content = str(exception) valeur et l’a enregistré dans une table Delta avec d’autres colonnes. C’est très courant. C’est aussi un cauchemar de test. Finalement, j’ai fini avec ceci :

from unittest import mock

...
    def setUp(self):
        # Mock the DataFrame and its write chain
        self.mock_df = mock.Mock()
        self.mock_write = mock.Mock()
        self.mock_df.write.format.return_value.mode.return_value.saveAsTable = self.mock_write

        # Patch the get_spark_session function
        self.patcher = mock.patch('main.utils.custom_logging.get_spark_session')
        self.mock_spark_session = self.patcher.start()
        self.mock_spark_session.return_value.createDataFrame.return_value = self.mock_df

    def test_log_info_message(self):
        """Test basic info logging."""
        log_message(message=self.TEST_MESSAGE)

        # Assert DataFrame creation and write process were called
        self.mock_spark_session.return_value.createDataFrame.assert_called_once()
        self.mock_df.write.format.assert_called_with("delta")
        self.mock_df.write.format.return_value.mode.assert_called_with("append")
        self.mock_write.assert_called_with("default.logging")

    def tearDown(self):
        self.patcher.stop()

J’ai également dû revenir au fichier python que je testais et retirer la session Spark de cette méthode. Pour des raisons, je suppose.

def _get_spark_session():
    spark = SparkSession.builder.appName(APP_NAME).getOrCreate()

C’était beaucoup de travail. C’est pourquoi j’inclus le code. Cependant, une grande partie du travail que j’ai fait consistait simplement à lancer des trucs. Alors n’utilisez pas le code que j’ai inclus. Il existe une bien meilleure solution. N’écrivez pas de code aussi difficile à tester.

Testez simplement les méthodes publiques. Gardez votre méthode publique propre : acceptez, validez et traitez les paramètres et portez une attention particulière aux pré-conditions, post-conditions et invariants. Mettez la logique de support et l’intégration dans des méthodes privées. Puisqu’il s’agit de python, vous n’obtenez pas vraiment de méthodes privées. J’utilise un seul trait de soulignement, qui est une convention de dénomination que d’autres développeurs comprendraient. N’utilisez pas le double trait de soulignement car honnêtement, je ne sais pas ce que signifie nom mutilé ferait dans Databricks. Une fois que j’ai déplacé le code Databricks vers un module privé, je n’ai pas eu besoin d’utiliser le code _get_spark_session. J’ai conservé les tests unitaires parce que je les ai écrits mais j’allais de toute façon découvrir toutes ces informations dans les tests unitaires.

Structure du projet

C’était une question difficile. Imaginez comment vos blocs-notes sont disposés dans Databricks. Se prête-t-il facilement à cette structure (trouvée dans votre fichier .vscode/.settings) ?

"python.testing.unittestArgs": [
    "-v",
    "-s",
    "./my_dir/test",
    "-p",
    "*_test.py"
],

Est-ce qu’il s’intègre dans une structure de répertoires qui pourrait être conforme à ./.venv/bin/python -m unittest discover -v -s ./my_dir/test -p '*_test.py'? Probablement pas. Mais c’est ce dont vous aurez besoin. Jusqu’à présent, j’ai parlé de TDD et un peu de repos. C’est là qu’intervient un pipeline CI/CD. Pour le développement, vous aurez besoin des répertoires principal et de test si vous souhaitez exécuter vos tests dans l’Explorateur de tests. De plus, vous devrez être explicite sur vos chemins dans vos importations. Cela va être difficile ; espérons que votre sed n’est pas trop rouillé 🙂

À un niveau élevé, le pipeline devra effectuer les opérations suivantes :

  1. Déclenchez un workflow. La poussée vers une branche spécifique est un modèle bien connu ici, mais il en existe d’autres.
  2. Extraire le dépôt : vous aurez besoin d’un workflow pour accéder au dépôt. L’étape actions/paiement est un bon début (pour Actions GitHub).
  3. Réorganiser les fichiers : choisissez quelque chose que vous connaissez, comme Python ou un script shell, pour aplatir la structure des répertoires, renommez les fichiers et modifiez les instructions d’importation concernées.
  4. Push to Databricks Repo : je recommande d’utiliser un Compte de service Databricks et utilisez l’API REST. La CLI Databricks est une option, mais c’est une chose de plus à installer et à maintenir et dans la plupart des environnements d’entreprise, quelqu’un quelque part dira non à un moment donné.

En fait, je préférerais que vous utilisiez un script shell plutôt que python simplement parce qu’il n’y aura aucune dépendance. Je sais que beaucoup de gens n’utilisent pas beaucoup la ligne de commande, et je me sens mal à propos de ce commentaire sed, alors voici un exemple de script bash pour aplatir vos répertoires et refactoriser les fichiers d’importation.

#!/bin/bash

# Rename directories
mv ./main/api_1 ./api_1
mv ./main/api_2 ./api_2

# Update import statements in Python files
find . -name '*.py' -exec sed -i 's/from main.api_1/from api_1/g' {} +
find . -name '*.py' -exec sed -i 's/from main.api_2/from api_2/g' {} +

Conclusion

Ces dernier des postes étaient assez durs. Je veux dire, la plupart des gens lancent simplement un bloc-notes Databricks et tout fonctionne. Cependant, rien ne « fonctionne ». Tout le monde a pensé que leur dette technique était une bonne idée à un moment donné. Vous devez tester votre code pour faire confiance à votre code. Les gens aiment souligner que les tests ne garantissent pas le succès. Personne que je connais qui teste beaucoup n’a jamais fait cette affirmation. Les tests unitaires valident simplement que votre implémentation particulière de votre interprétation de la description d’un besoin commercial par quelqu’un a fonctionné à un moment donné sur la base d’un nombre limité d’hypothèses bien définies. Si c’est tout ce que vous obtenez d’une centaine de tests unitaires, imaginez les manigances de ne pas tester du tout. J’ai également ajouté un pipeline CI/CD à la fin. Encore une fois, si un déploiement contrôlé peut être difficile, imaginez le chaos des déploiements incontrôlés.

Dans mon premier article de cette série, j’ai dit : « Je n’aime pas tester Briques de données des cahiers et c’est un problème. Plus important encore, je suis développeur depuis des décennies et j’ai vu ce qu’il faut pour construire des systèmes résilients et fiables. Mettre des ordinateurs portables en production sans garde-fou n’est pas une bonne base. Relisez ces blogs du point de vue d’un développeur construisant un système en dehors de Databricks. Un grand nombre de ces recommandations sont des bonnes pratiques Python habituelles. L’idée selon laquelle vous pouvez assouplir les meilleures pratiques de base simplement parce que vous utilisez un grand moteur distribué n’a objectivement aucun sens. Tout ce que j’ai recommandé ici est gratuit. Cela demande du travail, mais c’est pourquoi on appelle cela un travail.

Bonne chance et amusez-vous bien!






Source link