Comment puis-je appeler une commande Django manage.py personnalisée directement à partir d'un pilote de test?


174

Je souhaite écrire un test unitaire pour une commande Django manage.py qui effectue une opération de backend sur une table de base de données. Comment appeler la commande de gestion directement à partir du code?

Je ne veux pas exécuter la commande sur le shell du système d'exploitation à partir de tests.py car je ne peux pas utiliser l'environnement de test configuré à l'aide de manage.py test (base de données de test, boîte d'envoi d'e-mail factice de test, etc.)

Réponses:


312

La meilleure façon de tester de telles choses - extraire les fonctionnalités nécessaires de la commande elle-même vers une fonction ou une classe autonome. Il permet de faire abstraction des «trucs d'exécution de commande» et d'écrire des tests sans exigences supplémentaires.

Mais si, pour une raison quelconque, vous ne pouvez pas découpler la commande de formulaire logique, vous pouvez l'appeler à partir de n'importe quel code en utilisant la méthode call_command comme celle-ci:

from django.core.management import call_command

call_command('my_command', 'foo', bar='baz')

19
+1 pour placer la logique testable ailleurs (méthode du modèle? Méthode du gestionnaire? Fonction autonome?) Afin que vous n'ayez pas du tout besoin de jouer avec la machine call_command. Rend également la fonctionnalité plus facile à réutiliser.
Carl Meyer

36
Même si vous extrayez la logique, cette fonction est toujours utile pour tester le comportement de votre commande, comme les arguments requis, et pour vous assurer qu'elle appelle votre fonction de bibliothèque qui fait le vrai travail.
Igor Sobreira

Le premier paragraphe s'applique à toute situation limite. Déplacez votre propre code logique biz hors du code qui est contraint de s'interfacer avec quelque chose, comme un utilisateur. Cependant, si vous écrivez la ligne de code, cela pourrait avoir un bogue, donc les tests devraient en effet atteindre derrière n'importe quelle limite.
Phlip

Je pense que c'est toujours utile pour quelque chose comme call_command('check'), pour s'assurer que les vérifications du système sont réussies, dans un test.
Adam Barnes

22

Plutôt que de faire l'astuce call_command, vous pouvez exécuter votre tâche en faisant:

from myapp.management.commands import my_management_task
cmd = my_management_task.Command()
opts = {} # kwargs for your command -- lets you override stuff for testing...
cmd.handle_noargs(**opts)

10
Pourquoi feriez-vous cela alors que call_command fournit également la capture de stdin, stdout, stderr? Et quand la documentation précise la bonne façon de procéder?
boatcoder

17
C'est une très bonne question. Il y a trois ans j'aurais peut-être eu une réponse pour vous;)
Nate

1
Ditto Nate - quand sa réponse était ce que j'ai trouvé il y a un an et demi - je me suis simplement basé dessus ...
Danny Staple

2
Post-creuser, mais aujourd'hui cela m'a aidé: je n'utilise pas toujours toutes les applications de ma base de code (en fonction du site Django utilisé), et j'ai call_commandbesoin de l'application testée pour être chargée INSTALLED_APPS. Entre devoir charger l'application uniquement à des fins de test et l'utiliser, j'ai choisi ceci.
Mickaël

call_commandest probablement ce que la plupart des gens devraient essayer en premier. Cette réponse m'a aidé à contourner un problème où je devais passer des noms de table Unicode à la inspectdbcommande. python / bash interprétaient les arguments de ligne de commande comme ascii, et cela bombardait l' get_table_descriptionappel profondément dans django.
bigh_29


14

La documentation Django sur la commande call_command ne mentionne pas qui outdoit être redirigé vers sys.stdout. L'exemple de code doit lire:

from django.core.management import call_command
from django.test import TestCase
from django.utils.six import StringIO
import sys

class ClosepollTest(TestCase):
    def test_command_output(self):
        out = StringIO()
        sys.stdout = out
        call_command('closepoll', stdout=out)
        self.assertIn('Expected output', out.getvalue())

1

En m'appuyant sur la réponse de Nate, j'ai ceci:

def make_test_wrapper_for(command_module):
    def _run_cmd_with(*args):
        """Run the possibly_add_alert command with the supplied arguments"""
        cmd = command_module.Command()
        (opts, args) = OptionParser(option_list=cmd.option_list).parse_args(list(args))
        cmd.handle(*args, **vars(opts))
    return _run_cmd_with

Usage:

from myapp.management import mycommand
cmd_runner = make_test_wrapper_for(mycommand)
cmd_runner("foo", "bar")

L'avantage ici est que si vous avez utilisé des options supplémentaires et OptParse, cela réglera le problème pour vous. Ce n'est pas tout à fait parfait - et il ne dirige pas encore les sorties - mais il utilisera la base de données de test. Vous pouvez ensuite tester les effets de la base de données.

Je suis sûr que l'utilisation du module simulé Micheal Foords et le recâblage de stdout pendant la durée d'un test signifieraient que vous pourriez aussi tirer un peu plus de cette technique - tester la sortie, les conditions de sortie, etc.


4
Pourquoi voudriez-vous vous donner tous ces problèmes au lieu d'utiliser simplement call_command?
boatcoder
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.