Tests unitaires à Django

12

J'ai vraiment du mal à écrire des tests unitaires efficaces pour un grand projet Django. J'ai une couverture de test raisonnablement bonne, mais je me suis rendu compte que les tests que j'ai écrits sont définitivement des tests d'intégration / d'acceptation, pas des tests unitaires du tout, et j'ai des parties critiques de mon application qui ne sont pas testées efficacement. Je veux résoudre ce problème dès que possible.

Voici mon problème. Mon schéma est profondément relationnel et fortement axé sur le temps, ce qui donne à mon objet modèle un couplage interne élevé et beaucoup d'état. Beaucoup de mes méthodes de modèle interrogent en fonction des intervalles de temps, et j'ai beaucoup de auto_now_addchoses à faire dans les champs horodatés. Prenez par exemple une méthode qui ressemble à ceci:

def summary(self, startTime=None, endTime=None):
    # ... logic to assign a proper start and end time 
    # if none was provided, probably using datetime.now()

    objects = self.related_model_set.manager_method.filter(...)

    return sum(object.key_method(startTime, endTime) for object in objects)

Comment approche-t-on un test comme celui-ci?

Voici où j'en suis jusqu'à présent. Il me semble que l'objectif de test unitaire devrait être donné un comportement moqué by key_methodsur ses arguments, est-il summarycorrectement filtré / agrégé pour produire un résultat correct?

Se moquer de datetime.now () est assez simple, mais comment puis-je me moquer du reste du comportement?

  • Je pourrais utiliser des appareils, mais j'ai entendu des avantages et des inconvénients d'utiliser des appareils pour construire mes données (la mauvaise maintenabilité étant un con qui frappe chez moi).
  • Je pourrais également configurer mes données via l'ORM, mais cela peut être limitant, car je dois également créer des objets associés. Et l'ORM ne vous permet pas de jouer avec les auto_now_addchamps manuellement.
  • Se moquer de l'ORM est une autre option, mais non seulement il est difficile de se moquer des méthodes ORM profondément imbriquées, mais la logique dans le code ORM se moque du test, et la moquerie semble rendre le test vraiment dépendant des internes et des dépendances du fonction sous test.

Les écrous les plus difficiles à casser semblent être les fonctions comme celle-ci, qui reposent sur quelques couches de modèles et de fonctions de niveau inférieur et sont très dépendantes du temps, même si ces fonctions peuvent ne pas être super compliquées. Mon problème global est que, peu importe la façon dont je semble le découper, mes tests semblent beaucoup plus complexes que les fonctions qu'ils testent.

acjay
la source
Vous devez d'abord écrire des tests unitaires à partir de maintenant, cela vous aidera à repérer les problèmes de testabilité dans votre conception avant que le code de production réel ne soit écrit.
Chedy2149
2
C'est utile, mais cela ne résout pas vraiment la question de savoir comment tester au mieux les applications intrinsèquement riches en états et ORM.
acjay
Vous devez supprimer la couche de persistance
Chedy2149
1
Cela semble bien hypothétique, mais en ce qui concerne la maintenance du projet, je pense qu'il y a un coût non négligeable à insérer une couche de persistance sur mesure entre la logique métier et l'ORM Django extrêmement bien documenté. Du coup, les classes se remplissent d'un tas de minuscules méthodes intermédiaires qui doivent elles-mêmes être refactorisées au fil du temps. Mais cela est peut-être justifié dans les endroits où la testabilité est critique.
acjay

Réponses:

6

Je vais aller de l'avant et enregistrer une réponse pour ce que j'ai trouvé jusqu'à présent.

Mon hypothèse est que pour une fonction avec couplage profond et état, la réalité est que cela va simplement prendre beaucoup de lignes pour contrôler son contexte extérieur.

Voici à quoi ressemble mon cas de test, en s'appuyant sur la bibliothèque Mock standard:

  1. J'utilise l'ORM standard pour configurer la séquence d'événements
  2. Je crée mon propre départ datetimeet subvertis les auto_now_addtemps pour s'adapter à une chronologie fixe de ma conception. Je pensais que l'ORM ne le permettait pas, mais cela fonctionne bien.
  3. Je m'assure que la fonction sous test utilise from datetime import datetimepour que je puisse patcher datetime.now()juste cette fonction (si je moque toute la datetimeclasse, l'ORM fait un ajustement).
  4. Je crée mon propre remplacement pour object.key_method(), avec des fonctionnalités simples mais bien définies qui dépendent des arguments. Je veux que cela dépende des arguments, car sinon je ne saurais pas si la logique de la fonction en cours de test fonctionne. Dans mon cas, il renvoie simplement le nombre de secondes entre startTimeet endTime. Je object.key_method()le new_callablecorrige en l'enveloppant dans un lambda et en le corrigeant directement en utilisant le kwarg de patch.
  5. Enfin, je lance une série d'assertions sur divers appels de summaryavec différents arguments pour vérifier l'égalité avec les résultats attendus calculés à la main, tenant compte du comportement donné de la maquettekey_method

Il va sans dire que c'est beaucoup plus long et plus compliqué que la fonction elle-même. Cela dépend de la base de données et ne ressemble pas vraiment à un test unitaire. Mais il est également assez découplé des internes de la fonction - juste sa signature et ses dépendances. Je pense donc que ce pourrait être un test unitaire, quand même.

Dans mon application, la fonction est assez centrale et sujette à une refactorisation pour optimiser ses performances. Je pense donc que le problème en vaut la peine, malgré la complexité. Mais je suis toujours ouvert à de meilleures idées sur la façon d'aborder cela. Tout cela fait partie de mon long voyage vers un style de développement plus axé sur les tests ...

acjay
la source