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.)

MikeN
la source

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')
Alex Koshelev
la source
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)
Nate
la source
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
17

le code suivant:

from django.core.management import call_command
call_command('collectstatic', verbosity=3, interactive=False)
call_command('migrate', 'myapp', verbosity=3, interactive=False)

... est égal aux commandes suivantes tapées dans le terminal:

$ ./manage.py collectstatic --noinput -v 3
$ ./manage.py migrate myapp --noinput -v 3

Voir l' exécution des commandes de gestion à partir de la documentation django .

Artur Barseghyan
la source
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())
Alan Viars
la source
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.

Danny Staple
la source
4
Pourquoi voudriez-vous vous donner tous ces problèmes au lieu d'utiliser simplement call_command?
boatcoder