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

162

У меня есть модуль Python, который использует библиотеку argparse. Как мне написать тесты для этого раздела базы кода?

pydanny
источник
argparse - это интерфейс командной строки. Напишите свои тесты для вызова приложения через командную строку.
Homer6
Ваш вопрос затрудняет понимание того, что вы хотите проверить. Я подозреваю, что это в конечном счете, например, «когда я использую аргументы командной строки X, Y, Z, тогда foo()вызывается функция ». Насмешка sys.argv- это ответ, если это так. Взгляните на пакет Python cli-test-helpers . См. Также stackoverflow.com/a/58594599/202834
Петерино

Ответы:

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, представляющий имя сценария, удален, чтобы не отправлять его в качестве дополнительного переключателя во время работы интерфейса командной строки.)

В ваших тестах вы можете затем вызвать функцию синтаксического анализатора с любым списком аргументов, с которым вы хотите протестировать ее:

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

«argparse part» немного расплывчатый, поэтому этот ответ фокусируется на одной части: 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())
Munsu
источник
Но теперь ваш код юнит-теста также зависит argparseи от его Namespaceкласса. Вы должны издеваться Namespace.
imrek
1
@DrunkenMaster приносит извинения за язвительный тон. Я обновил свой ответ с объяснением и возможным использованием. Я также учусь здесь, так что если вы могли бы, можете ли вы (или кто-то еще) привести случаи, когда насмешка над возвращаемым значением выгодна? (или, по крайней мере, в случаях, когда отсутствие насмешек над возвращаемым значением вредно)
munsu
1
from unittest import mockтеперь правильный метод импорта - по крайней мере, для python3
Майкл Холл,
1
@MichaelHall спасибо. Я обновил фрагмент и добавил контекстную информацию.
Мунсу
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. Вызов из пакетного / bash-файла с вашими флагами и флагом сброса аргументов.
  3. Поместите все ваши аргументы парсинга в отдельный файл и в if __name__ == "__main__":синтаксический анализ вызова и дампа / оценки результатов, а затем проверить это из пакетного / bash-файла.
Стив Барнс
источник
9

Я не хотел изменять оригинальный сценарий обслуживания, поэтому я просто sys.argvвырезал часть в argparse.

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

hpaulj
источник
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функции я иногда использую 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 == "?"
    ...
vczm
источник