Come si scrivono i test per la parte argparse di un modulo Python? [chiuso]


162

Ho un modulo Python che utilizza la libreria argparse. Come scrivo i test per quella sezione della base di codice?


argparse è un'interfaccia a riga di comando. Scrivi i tuoi test per invocare l'applicazione tramite la riga di comando.
Homer6

La tua domanda rende difficile capire cosa vuoi testare. Sospetto che alla fine lo sia, ad esempio "quando uso gli argomenti della riga di comando X, Y, Z allora foo()viene chiamata la funzione ". Deridere sys.argvè la risposta se è così. Dai un'occhiata al pacchetto Python di cli-test-helpers . Vedi anche stackoverflow.com/a/58594599/202834
Peterino

Risposte:


214

È necessario refactificare il codice e spostare l'analisi su una funzione:

def parse_args(args):
    parser = argparse.ArgumentParser(...)
    parser.add_argument...
    # ...Create your parser as you like...
    return parser.parse_args(args)

Quindi nella tua mainfunzione dovresti semplicemente chiamarlo con:

parser = parse_args(sys.argv[1:])

(dove il primo elemento sys.argvrappresenta il nome dello script viene rimosso per non inviarlo come switch aggiuntivo durante l'operazione CLI.)

Nei tuoi test, puoi quindi chiamare la funzione parser con qualsiasi elenco di argomenti con cui vuoi testarlo:

def test_parser(self):
    parser = parse_args(['-l', '-m'])
    self.assertTrue(parser.long)
    # ...and so on.

In questo modo non dovrai mai eseguire il codice dell'applicazione solo per testare il parser.

Se è necessario modificare e / o aggiungere opzioni al parser in un secondo momento nell'applicazione, creare un metodo di fabbrica:

def create_parser():
    parser = argparse.ArgumentParser(...)
    parser.add_argument...
    # ...Create your parser as you like...
    return parser

In seguito puoi manipolarlo se vuoi, e un test potrebbe apparire come:

class ParserTest(unittest.TestCase):
    def setUp(self):
        self.parser = create_parser()

    def test_something(self):
        parsed = self.parser.parse_args(['--something', 'test'])
        self.assertEqual(parsed.something, 'test')

4
Grazie per la tua risposta. Come testiamo gli errori quando non viene passato un determinato argomento?
Pratik Khadloya,

3
@PratikKhadloya Se l'argomento è obbligatorio e non viene passato, argparse genererà un'eccezione.
Viktor Kerkez,

2
@PratikKhadloya Sì, purtroppo il messaggio non è molto utile :( È solo 2... argparsenon è molto test-friendly poiché stampa direttamente su sys.stderr...
Viktor Kerkez

1
@ViktorKerkez Potresti essere in grado di deridere sys.stderr per verificare un messaggio specifico, mock.assert_called_with o esaminando mock_calls, vedi docs.python.org/3/library/unittest.mock.html per maggiori dettagli. Vedi anche stackoverflow.com/questions/6271947/… per un esempio di beffardo stdin. (stderr dovrebbe essere simile)
BryCoBat,

1
@PratikKhadloya vedi la mia risposta per errori di gestione / test stackoverflow.com/a/55234595/1240268
Andy Hayden

25

"Argparse porzione" è un po 'vaga, quindi questa risposta si concentra su una parte: il parse_argsmetodo. Questo è il metodo che interagisce con la riga di comando e ottiene tutti i valori passati. Fondamentalmente, puoi prendere in giro ciò che parse_argsritorna in modo che non sia necessario effettivamente ottenere valori dalla riga di comando. Il mock pacchetto può essere installato tramite pip per le versioni 2.6-3.2 di Python. Fa parte della libreria standard a partire unittest.mockdalla versione 3.3.

import argparse
try:
    from unittest import mock  # python 3.3+
except ImportError:
    import mock  # python 2.6-3.2


@mock.patch('argparse.ArgumentParser.parse_args',
            return_value=argparse.Namespace(kwarg1=value, kwarg2=value))
def test_command(mock_args):
    pass

Devi includere tutti gli argomenti del tuo metodo di comando Namespace anche se non vengono passati. Dai a questi argomenti un valore di None. (consultare i documenti ) Questo stile è utile per eseguire rapidamente test per i casi in cui vengono passati valori diversi per ogni argomento del metodo. Se si sceglie di deridere Namespacese stesso per la totale mancanza di fiducia nei test, assicurarsi che si comporti in modo simile alla Namespaceclasse effettiva .

Di seguito è riportato un esempio che utilizza il primo frammento della libreria argparse.

# test_mock_argparse.py
import argparse
try:
    from unittest import mock  # python 3.3+
except ImportError:
    import mock  # python 2.6-3.2


def main():
    parser = argparse.ArgumentParser(description='Process some integers.')
    parser.add_argument('integers', metavar='N', type=int, nargs='+',
                        help='an integer for the accumulator')
    parser.add_argument('--sum', dest='accumulate', action='store_const',
                        const=sum, default=max,
                        help='sum the integers (default: find the max)')

    args = parser.parse_args()
    print(args)  # NOTE: this is how you would check what the kwargs are if you're unsure
    return args.accumulate(args.integers)


@mock.patch('argparse.ArgumentParser.parse_args',
            return_value=argparse.Namespace(accumulate=sum, integers=[1,2,3]))
def test_command(mock_args):
    res = main()
    assert res == 6, "1 + 2 + 3 = 6"


if __name__ == "__main__":
    print(main())

Ma ora dipende anche il tuo codice unittest argparsee la sua Namespaceclasse. Dovresti deridere Namespace.
imrek,

1
@DrunkenMaster si scusa per il tono snarky. Ho aggiornato la mia risposta con spiegazioni e possibili usi. Sto imparando anche qui, quindi se tu potessi, (o qualcun altro) puoi fornire casi in cui deridere il valore di ritorno è vantaggioso? (o almeno nei casi in cui non deridere il valore di ritorno è dannoso)
munsu

1
from unittest import mockè ora il metodo di importazione corretto - almeno per python3
Michael Hall

1
@MichaelHall grazie. Ho aggiornato lo snippet e aggiunto informazioni contestuali.
munsu,

1
L'uso della Namespaceclasse qui è esattamente quello che stavo cercando. Nonostante il test argparsefaccia ancora affidamento , non si basa sulla particolare implementazione del argparsecodice in prova, che è importante per i miei test unitari. Inoltre, è facile usare pytestil parametrize()metodo per testare rapidamente varie combinazioni di argomenti con un modello simulato che include return_value=argparse.Namespace(accumulate=accumulate, integers=integers).
acetone,

17

Fai in modo che la tua main()funzione prenda argvcome argomento anziché lasciarla leggere sys.argvcome sarà di default :

# mymodule.py
import argparse
import sys


def main(args):
    parser = argparse.ArgumentParser()
    parser.add_argument('-a')
    process(**vars(parser.parse_args(args)))
    return 0


def process(a=None):
    pass

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

Quindi puoi testare normalmente.

import mock

from mymodule import main


@mock.patch('mymodule.process')
def test_main(process):
    main([])
    process.assert_call_once_with(a=None)


@mock.patch('foo.process')
def test_main_a(process):
    main(['-a', '1'])
    process.assert_call_once_with(a='1')

9
  1. Popola la tua lista arg usando sys.argv.append()e poi chiama parse(), controlla i risultati e ripeti.
  2. Chiama da un file batch / bash con i tuoi flag e un dump args flag.
  3. Metti tutti i tuoi argomenti di analisi in un file separato e nel if __name__ == "__main__":call parse e scarica / valuta i risultati, quindi prova questo da un file batch / bash.

9

Non volevo modificare lo script di pubblicazione originale, quindi ho semplicemente deriso la sys.argvparte in argparse.

from unittest.mock import patch

with patch('argparse._sys.argv', ['python', 'serve.py']):
    ...  # your test code here

Ciò si interrompe se l'implementazione di argparse cambia ma abbastanza per uno script di test rapido. La sensibilità è comunque molto più importante della specificità negli script di test.


6

Un modo semplice per testare un parser è:

parser = ...
parser.add_argument('-a',type=int)
...
argv = '-a 1 foo'.split()  # or ['-a','1','foo']
args = parser.parse_args(argv)
assert(args.a == 1)
...

Un altro modo è modificare sys.argve chiamareargs = parser.parse_args()

Ci sono molti esempi di test argparseinlib/test/test_argparse.py


5

parse_argslancia SystemExitae stampa su stderr, puoi catturare entrambi:

import contextlib
import io
import sys

@contextlib.contextmanager
def captured_output():
    new_out, new_err = io.StringIO(), io.StringIO()
    old_out, old_err = sys.stdout, sys.stderr
    try:
        sys.stdout, sys.stderr = new_out, new_err
        yield sys.stdout, sys.stderr
    finally:
        sys.stdout, sys.stderr = old_out, old_err

def validate_args(args):
    with captured_output() as (out, err):
        try:
            parser.parse_args(args)
            return True
        except SystemExit as e:
            return False

Ispezioni stderr (usando err.seek(0); err.read()ma generalmente non è richiesta la granularità.

Ora puoi usare assertTrueo qualunque test ti piaccia:

assertTrue(validate_args(["-l", "-m"]))

In alternativa, potresti voler catturare e ricodificare un errore diverso (anziché SystemExit):

def validate_args(args):
    with captured_output() as (out, err):
        try:
            return parser.parse_args(args)
        except SystemExit as e:
            err.seek(0)
            raise argparse.ArgumentError(err.read())

2

Quando si passano i risultati da argparse.ArgumentParser.parse_argsa una funzione, a volte uso a namedtupleper deridere argomenti per i test.

import unittest
from collections import namedtuple
from my_module import main

class TestMyModule(TestCase):

    args_tuple = namedtuple('args', 'arg1 arg2 arg3 arg4')

    def test_arg1(self):
        args = TestMyModule.args_tuple("age > 85", None, None, None)
        res = main(args)
        assert res == ["55289-0524", "00591-3496"], 'arg1 failed'

    def test_arg2(self):
        args = TestMyModule.args_tuple(None, [42, 69], None, None)
        res = main(args)
        assert res == [], 'arg2 failed'

if __name__ == '__main__':
    unittest.main()

0

Per testare l'interfaccia della riga di comando (interfaccia della riga di comando) e non l'output del comando ho fatto qualcosa del genere

import pytest
from argparse import ArgumentParser, _StoreAction

ap = ArgumentParser(prog="cli")
ap.add_argument("cmd", choices=("spam", "ham"))
ap.add_argument("-a", "--arg", type=str, nargs="?", default=None, const=None)
...

def test_parser():
    assert isinstance(ap, ArgumentParser)
    assert isinstance(ap, list)
    args = {_.dest: _ for _ in ap._actions if isinstance(_, _StoreAction)}
    
    assert args.keys() == {"cmd", "arg"}
    assert args["cmd"] == ("spam", "ham")
    assert args["arg"].type == str
    assert args["arg"].nargs == "?"
    ...
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.