Як ви пишете тести для частини argparse модуля python? [зачинено]


162

У мене є модуль Python, який використовує бібліотеку argparse. Як написати тести для цього розділу кодової бази?


argparse - це інтерфейс командного рядка. Напишіть свої тести, щоб викликати програму через командний рядок.
Homer6

Ваше запитання ускладнює розуміння того, що ви хочете перевірити. Я б підозрював, що це в кінцевому підсумку, наприклад, "коли я використовую аргументи командного рядка X, Y, Z, тоді функція foo()викликається". Знущання над sys.argvвідповіддю, якщо це так. Погляньте на пакунок -тест-помічників Python. Дивіться також stackoverflow.com/a/58594599/202834
Peterino

Відповіді:


214

Вам слід переробити код і перемістити синтаксичний аналіз на функцію:

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

Тоді у своїй mainфункції слід просто зателефонувати за допомогою:

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

(де перший елемент, sys.argvщо представляє ім'я сценарію, видаляється, щоб не надсилати його як додатковий комутатор під час роботи CLI.)

Після цього у своїх тестах ви можете викликати функцію парсера з будь-яким списком аргументів, з яким ви хочете перевірити його:

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

Таким чином, вам ніколи не доведеться виконувати код вашої програми лише для тестування аналізатора.

Якщо вам потрібно змінити та / або додати параметри до свого аналізатора пізніше у програмі, тоді створіть заводський метод:

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

Пізніше ви можете маніпулювати ним, якщо хочете, і тест може виглядати так:

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
Дякую за вашу відповідь. Як ми перевіряємо на помилки, коли певний аргумент не передається?
Пратік Хадлоя

3
@PratikKhadloya Якщо аргумент необхідний і він не буде переданий, argparse створить виняток.
Віктор Керкез

2
@PratikKhadloya Так, повідомлення, на жаль, не дуже корисне :( Це просто 2... argparseне дуже тестово, оскільки друкується безпосередньо на sys.stderr...
Віктор Керкез

1
@ViktorKerkez Можливо, ви зможете знущатися над sys.stderr, щоб перевірити наявність конкретного повідомлення, або mock.assert_called_with, або дослідивши mock_calls, детальніше див. Docs.python.org/3/library/unittest.mock.html . Дивіться також stackoverflow.com/questions/6271947/… для прикладу знущань над stdin. (stderr має бути схожим)
BryCoBat

1
@PratikKhadloya дивіться мою відповідь на помилки обробки / тестування stackoverflow.com/a/55234595/1240268
Енді Хейден

25

"аргументаційна частина" трохи розпливчаста, тому ця відповідь зосереджена на одній частині: parse_argsметоді. Це метод, який взаємодіє з вашим командним рядком і отримує всі передані значення. В основному, ви можете знущатися над тим, що parse_argsповертається, щоб не потрібно було насправді отримувати значення з командного рядка. mock Пакет може бути встановлений через піп для пітона версії 2,6-3,2. Це частина стандартної бібліотеки починаючи unittest.mockз версії 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

Ви повинні включити всі аргументи вашого методу командування, Namespace навіть якщо вони не передані. Дайте цим аргументам значення None. (див. документи ) Цей стиль корисний для швидкого тестування у випадках, коли для кожного аргументу методу передаються різні значення. Якщо ви вирішили знущатися над Namespaceсамим повним невиконанням аргументів у своїх тестах, переконайтеся, що він поводиться аналогічно фактичному Namespaceкласу.

Нижче наводиться приклад використання першого фрагмента з бібліотеки 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())

Але тепер ваш одиничний код також залежить від argparseйого Namespaceкласу. Вам слід знущатися Namespace.
imrek

1
@DrunkenMaster вибачається за химерний тон. Я оновив свою відповідь поясненням та можливим використанням. Я тут також навчаюся, тож якщо ви (чи хтось інший) можете надати випадки, коли знущання із поверненої вартості вигідно? (або принаймні випадки, коли не знущання над поверненою величиною є згубним)
munsu

1
from unittest import mockтепер правильний метод імпорту - добре принаймні для python3
Michael Hall

1
@MichaelHall дякую. Я оновив фрагмент і додав інформацію про контекст.
munsu

1
Використання Namespaceкласу тут саме те, що я шукав. Незважаючи на тест, на який все ще покладаються argparse, він не покладається на конкретну реалізацію argparseтестового коду, що важливо для моїх тестових одиниць. Крім того, це просте у використанні pytest«s parametrize()способу швидко перевірити різні комбінації аргументів з шаблонним макетом , який включає в себе return_value=argparse.Namespace(accumulate=accumulate, integers=integers).
ацетон

17

Зробіть свою main()функцію сприйнятою якargv аргумент, а не дозволяючи їй читати так, sys.argvяк буде за замовчуванням :

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

Тоді ви можете нормально протестувати.

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. Наповніть список аргументів за допомогою, sys.argv.append()а потім зателефонуйте parse(), перевірте результати та повторіть.
  2. Зателефонуйте з файлу batch / bash зі своїми прапорами та прапором dump args.
  3. Покладіть увесь синтаксичний аналіз аргументу в окремий файл та в if __name__ == "__main__":розбір викликів та вивантажте / оцініть результати, а потім протестуйте це з файлу batch / bash.

9

Я не хотів змінювати оригінальний скрипт обслуговування, тому просто знущався над sys.argvчастиною в аргументації.

from unittest.mock import patch

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

Це руйнується, якщо зміна впровадження argparse змінюється, але достатньо для швидкого тестового сценарію. Чутливість набагато важливіша, ніж конкретизація тестових сценаріїв.


6

Простий спосіб тестування аналізатора:

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

Ще один спосіб - змінити sys.argvта зателефонуватиargs = parser.parse_args()

Прикладів тестування argparseвlib/test/test_argparse.py


5

parse_argsкидає а SystemExitта друкує на stderr, ви можете зловити і те й інше:

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

Ви оглядаєте stderr (використовуючи err.seek(0); err.read() але, як правило, деталізацію не потрібно.

Тепер ви можете використовувати assertTrueабо тестувати, що вам подобається:

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

Крім того, ви можете захотіти зловити та повторно відкинути іншу помилку (замість 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

Під час передачі результатів argparse.ArgumentParser.parse_argsдо функції я іноді використовую a namedtupleдля знущання над аргументами для тестування.

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

Для тестування CLI (інтерфейс командного рядка), а не виведення команди я зробив щось подібне

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 == "?"
    ...
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.