Tests unitaires pour les pipelines de transfert de données constitués de fonctions unifilaires

10

En lisant l' introduction pratique de Mary Rose Cook à la programmation fonctionnelle , elle donne comme exemple un anti-modèle

def format_bands(bands):
    for band in bands:
        band['country'] = 'Canada'
        band['name'] = band['name'].replace('.', '')
        band['name'] = band['name'].title()

depuis

  • la fonction fait plus d'une chose
  • le nom n'est pas descriptif
  • il a des effets secondaires

Comme solution proposée, elle suggère de canaliser les fonctions anonymes

pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
                      call(lambda x: x.replace('.', ''), 'name'),
                      call(str.title, 'name')])

Cependant, cela me semble avoir l'inconvénient d'être encore moins testable; au moins format_bands pourrait avoir un test unitaire pour vérifier s'il fait ce qu'il est censé faire, mais comment tester le pipeline? Ou est-ce l'idée que les fonctions anonymes sont si explicites qu'elles n'ont pas besoin d'être testées?

Mon application réelle pour cela est d'essayer de rendre mon pandascode plus fonctionnel. Je vais souvent avoir une sorte de pipeline à l'intérieur d'une fonction "munging" "

def munge_data(df)
     df['name'] = df['name'].str.lower()
     df = df.drop_duplicates()
     return df

Ou réécriture dans le style pipeline:

def munge_data(df)
    munged = (df.assign(lambda x: x['name'].str.lower()
                .drop_duplicates())
    return munged

Des suggestions de bonnes pratiques dans ce genre de situation?

Max Flander
la source
4
Ces fonctions lambda individuelles sont trop petites pour un test unitaire. Testez le résultat final. En d'autres termes, les fonctions anonymes ne sont pas testables à l'unité, donc n'écrivez pas la fonction comme une fonction anonyme si vous prévoyez de la tester individuellement.
Robert Harvey

Réponses:

1

Je pense que vous avez probablement manqué la partie la plus importante de l'exemple corrigé du livre. Le changement le plus fondamental du code passe de la méthode fonctionnant sur toutes les valeurs d'une liste à fonctionnant sur un élément.

Il existe déjà des fonctions comme iter(dans ce cas nommé pipeline_foreach) qui effectuent une opération donnée sur tous les éléments d'une liste. Il n'était pas nécessaire de dupliquer cela avec une forboucle. L'utilisation d'une opération de liste bien connue rend également votre intention claire. Avec mapvous transformez les valeurs. Avec itervous effectuez un effet secondaire avec chaque élément. Avec la forboucle, vous êtes ... eh bien, vous ne savez pas vraiment jusqu'à ce que vous regardiez à travers.

L'exemple de code corrigé n'est toujours pas très fonctionnel, car il (pour autant que je sache) mute les valeurs de la liste sans les renvoyer, ce qui empêche la composition de la tuyauterie ou des fonctions. La méthode fonctionnellement préférée mapcréerait une nouvelle liste de bandes avec les mises à jour countryet name. Ensuite, vous pouvez diriger cette sortie vers la fonction suivante ou composer mapavec une autre fonction qui a pris une liste de bandes. Avec iter, c'est comme une impasse de pipelining.

Je pense que le code du résultat final a de petites fonctions qui sont trop triviales pour déranger les tests ici. Après tout, vous ne devriez pas avoir besoin d'écrire des tests unitaires contre replaceou title. Maintenant, vous voulez peut-être les composer ensemble dans votre propre test de fonction et d'unité que la combinaison souhaitée est obtenue sur un seul élément. Moi-même, je serais probablement passé format_bandsau format_bandsingulier, abandonné la boucle for et appelé pipeline_each(bands, format_band). Ensuite, vous pouvez tester format_band pour vous assurer que vous n'avez pas oublié quelque chose.

Quoi qu'il en soit, passez à votre code. Votre deuxième exemple de code semble plus pipeline-y. Mais cela seul ne fournit pas les avantages d'une programmation fonctionnelle. En pratique, la programmation fonctionnelle signifie assurer la compatibilité des fonctions avec d'autres fonctions en définissant leur compatibilité uniquement en termes d'entrées et de sorties. S'il y a des effets secondaires cachés à l'intérieur de la fonction, alors malgré son entrée / sortie alignée avec une autre fonction, vous ne pouvez pas savoir s'ils sont compatibles jusqu'à l'exécution. Si toutefois, deux fonctions sont sans effet secondaire et correspondent à la sortie-à-l'entrée, vous pouvez les canaliser ou les composer avec peu de soucis de résultats inattendus.

Kasey Speakman
la source