Jak piszesz testy dla części argparse modułu Pythona? [Zamknięte]

162

Mam moduł Pythona, który używa biblioteki argparse. Jak napisać testy dla tej sekcji bazy kodu?

pydanny
źródło
argparse to interfejs wiersza poleceń. Napisz testy, aby wywołać aplikację za pośrednictwem wiersza poleceń.
Homer6,
Twoje pytanie utrudnia zrozumienie, co chcesz przetestować. Podejrzewałbym, że ostatecznie tak jest, np. „Kiedy używam argumentów wiersza poleceń X, Y, Z foo()to wywoływana jest funkcja ”. Kpiny sys.argvjest odpowiedzią, jeśli tak jest. Spójrz na pakiet cli-test-helpers Python. Zobacz także stackoverflow.com/a/58594599/202834
Peterino,

Odpowiedzi:

214

Powinieneś refaktoryzować swój kod i przenieść parsowanie do funkcji:

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

Następnie w swojej mainfunkcji powinieneś po prostu wywołać to za pomocą:

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

(gdzie pierwszy element sys.argvreprezentujący nazwę skryptu jest usuwany, aby nie wysyłać go jako dodatkowego przełącznika podczas operacji CLI).

W swoich testach możesz następnie wywołać funkcję parsera z dowolną listą argumentów, z którymi chcesz ją przetestować:

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

W ten sposób nigdy nie będziesz musiał wykonywać kodu swojej aplikacji tylko po to, aby przetestować parser.

Jeśli chcesz później zmienić i / lub dodać opcje do parsera w aplikacji, utwórz metodę fabryczną:

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

Jeśli chcesz, możesz później nim manipulować, a test może wyglądać następująco:

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')
Viktor Kerkez
źródło
4
Dziękuję za odpowiedź. W jaki sposób testujemy pod kątem błędów, gdy określony argument nie jest przekazywany?
Pratik Khadloya,
3
@PratikKhadloya Jeśli argument jest wymagany i nie został przekazany, argparse zgłosi wyjątek.
Viktor Kerkez
2
@PratikKhadloya Tak, wiadomość niestety nie jest zbyt pomocna :( Po prostu 2... argparsenie jest zbyt przyjazny testowi, ponieważ drukuje bezpośrednio do sys.stderr...
Viktor Kerkez
1
@ViktorKerkez Możesz udawać sys.stderr, aby sprawdzić konkretną wiadomość, mock.assert_called_with lub sprawdzając mock_calls, zobacz docs.python.org/3/library/unittest.mock.html, aby uzyskać więcej szczegółów. Zobacz także stackoverflow.com/questions/6271947/…, aby zapoznać się z przykładem mockowania stdin. (stderr powinno być podobne)
BryCoBat
1
@PratikKhadloya zobacz moją odpowiedź dla obsługi / błędy testowania stackoverflow.com/a/55234595/1240268
Andy Hayden
25

„część argparse” jest nieco niejasna, więc ta odpowiedź skupia się na jednej części: parse_argsmetodzie. Jest to metoda, która współdziała z wierszem poleceń i pobiera wszystkie przekazane wartości. Zasadniczo można kpić z tego, co parse_argszwraca, aby nie musiało faktycznie pobierać wartości z wiersza poleceń. mock Pakiet można zainstalować poprzez pip dla wersji Pythona 2.6-3.2. Jest częścią biblioteki standardowej począwszy unittest.mockod wersji 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

Musisz uwzględnić wszystkie argumenty metody poleceń, Namespace nawet jeśli nie zostały one przekazane. Nadaj tym argumentom wartość None. (zobacz dokumentację ) Ten styl jest przydatny do szybkiego testowania przypadków, w których dla każdego argumentu metody przekazywane są różne wartości. Jeśli zdecydujesz się kpić z Namespacesamego siebie, aby w testach całkowicie nie polegać na argparse, upewnij się, że zachowuje się podobnie do rzeczywistej Namespaceklasy.

Poniżej znajduje się przykład wykorzystujący pierwszy fragment z biblioteki 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())
munsu
źródło
Ale teraz twój najbardziej niezatarty kod zależy również od argparsei jego Namespaceklasy. Powinieneś kpić Namespace.
imrek
1
@DrunkenMaster przeprasza za złośliwy ton. Zaktualizowałem moją odpowiedź o wyjaśnienia i możliwe zastosowania. Tutaj też się uczę, więc jeśli chcesz, czy możesz (lub ktoś inny) podać przypadki, w których kpienie z wartości zwracanej jest korzystne? (lub co najmniej przypadkach nie drwiących wartość powrotna jest szkodliwa)
Munsu
1
from unittest import mockjest teraz poprawną metodą importu - przynajmniej dla pythona3
Michael Hall
1
@MichaelHall thanks. Zaktualizowałem fragment i dodałem informacje kontekstowe.
munsu
1
Zastosowanie tej Namespaceklasy jest dokładnie tym, czego szukałem. Mimo, że test wciąż się argparseopiera, nie opiera się na konkretnej implementacji argparsetestowanego kodu, co jest ważne dla moich testów jednostkowych. Ponadto, jest to łatwe w użyciu pytest„s parametrize()metody, aby szybko przetestować różne kombinacje argument z matrycy mock, który zawiera return_value=argparse.Namespace(accumulate=accumulate, integers=integers).
aceton
17

Niech twoja main()funkcja będzie traktowana argvjako argument, zamiast pozwolić jej czytać sys.argvtak, jak będzie domyślnie :

# 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:]))

Następnie możesz normalnie przetestować.

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')
Ceasar Bautista
źródło
9
  1. Wypełnij listę argumentów za pomocą, sys.argv.append()a następnie zadzwoń parse(), sprawdź wyniki i powtórz.
  2. Wywołanie z pliku wsadowego / bash z twoimi flagami i flagą dump args.
  3. Umieść wszystkie analizowane argumenty w oddzielnym pliku, a następnie if __name__ == "__main__":przeanalizuj wywołanie i zrzuć / oceń wyniki, a następnie przetestuj to z pliku wsadowego / bash.
Steve Barnes
źródło
9

Nie chciałem modyfikować oryginalnego skryptu serwującego, więc po prostu wyszydziłem sys.argvczęść w argparse.

from unittest.mock import patch

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

To zepsuje się, jeśli argparse zmieni się w implementacji, ale wystarczy do szybkiego skryptu testowego. W każdym razie wrażliwość jest znacznie ważniejsza niż konkretność w skryptach testowych.

김민준
źródło
6

Prosty sposób na przetestowanie parsera to:

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

Innym sposobem jest modyfikacja sys.argvi dzwonienieargs = parser.parse_args()

Istnieje wiele przykładów testowania argparsewlib/test/test_argparse.py

hpaulj
źródło
5

parse_argswyrzuca a SystemExiti drukuje na stderr, możesz złapać oba z nich:

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

Sprawdzasz stderr (używając, err.seek(0); err.read()ale generalnie ta szczegółowość nie jest wymagana.

Teraz możesz użyć assertTruelub dowolnego testu, który Ci się podoba:

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

Alternatywnie możesz chcieć wyłapać i ponownie zgłosić inny błąd (zamiast 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())
Andy Hayden
źródło
2

Podczas przekazywania wyników z argparse.ArgumentParser.parse_argsdo funkcji czasami używam a, namedtupleaby podrobić argumenty do testowania.

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()
Gość
źródło
0

Aby przetestować CLI (interfejs wiersza poleceń), a nie wyjście polecenia , zrobiłem coś takiego

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 == "?"
    ...
vczm
źródło