Como posso chamar um comando Django manage.py personalizado diretamente de um driver de teste?

174

Eu quero escrever um teste de unidade para um comando Django manage.py que faz uma operação de back-end em uma tabela de banco de dados. Como eu chamaria o comando de gerenciamento diretamente do código?

Não quero executar o comando no shell do sistema operacional a partir de tests.py porque não posso usar o ambiente de teste configurado usando o manage.py test (banco de dados de teste, caixa de saída de email fictícia de teste, etc ...)

MikeN
fonte

Respostas:

312

A melhor maneira de testar essas coisas - extrair a funcionalidade necessária do próprio comando para a função ou classe autônoma. Ajuda a abstrair do "material de execução de comando" e escrever o teste sem requisitos adicionais.

Mas se por algum motivo você não conseguir dissociar o comando do formulário lógico, poderá chamá-lo de qualquer código usando o método call_command como este:

from django.core.management import call_command

call_command('my_command', 'foo', bar='baz')
Alex Koshelev
fonte
19
+1 para colocar a lógica testável em outro lugar (método de modelo? Método de gerente? Função autônoma?) Para que você não precise mexer com o mecanismo de comando de chamada. Também facilita a reutilização da funcionalidade.
Carl Meyer
36
Mesmo se você extrair a lógica, essa função ainda será útil para testar o comportamento específico de seu comando, como os argumentos necessários, e para garantir que ela chame a função de biblioteca, pois ele faz o trabalho real.
Igor Sobreira
O parágrafo de abertura se aplica a qualquer situação de fronteira. Mova seu próprio código lógico de negócios para fora do código restrito à interface com algo, como um usuário. No entanto, se você escrever a linha de código, pode haver um erro, portanto os testes devem realmente ultrapassar qualquer limite.
Phlip
Eu acho que isso ainda é útil para algo como call_command('check'), para garantir que as verificações do sistema sejam aprovadas em um teste.
Adam Barnes
22

Em vez de executar o truque call_command, você pode executar sua tarefa executando:

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
fonte
10
Por que você faria isso quando call_command também fornece a captura de stdin, stdout, stderr? E quando a documentação especifica a maneira correta de fazer isso?
boatcoder 21/09
17
Essa é uma pergunta extremamente boa. Três anos atrás, talvez eu tivesse uma resposta para você;)
Nate
1
Ditto Nate - quando sua resposta foi que eu encontrei um ano e meio atrás - Eu simplesmente construída sobre ela ...
Danny Staple
2
Após a escavação, mas hoje isso me ajudou: nem sempre estou usando todos os aplicativos da minha base de código (dependendo do site do Django usado) e call_commandprecisa que o aplicativo testado seja carregado INSTALLED_APPS. Entre ter que carregar o aplicativo apenas para fins de teste e usar isso, eu escolhi isso.
Mickaël
call_commandé provavelmente o que a maioria das pessoas deve tentar primeiro. Esta resposta me ajudou a solucionar um problema em que eu precisava passar nomes de tabelas unicode para o inspectdbcomando. python / bash estava interpretando argumentos da linha de comando como ascii, e isso estava bombardeando a get_table_descriptionchamada profundamente no django.
bigh_29
17

o seguinte código:

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

... é igual aos seguintes comandos digitados no terminal:

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

Veja executando comandos de gerenciamento no django docs .

Artur Barseghyan
fonte
14

A documentação do Django no call_command falha ao mencionar que outdeve ser redirecionada para sys.stdout. O código de exemplo deve ser:

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
fonte
1

Com base na resposta de Nate, tenho o seguinte:

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

Uso:

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

A vantagem aqui é que, se você usou opções adicionais e o OptParse, isso resolverá o problema. Não é perfeito - e ainda não canaliza resultados -, mas usará o banco de dados de teste. Você pode testar os efeitos do banco de dados.

Estou certo de que o uso do módulo de simulação Micheal Foords e também a reconexão do stdout durante a duração de um teste significariam que você também poderia aproveitar um pouco mais dessa técnica - teste a saída, as condições de saída etc.

Danny Staple
fonte
4
Por que você enfrentaria todo esse problema em vez de apenas usar o comando call_command?
boatcoder