파이썬 모듈의 argparse 부분에 대한 테스트를 어떻게 작성합니까? [닫은]


162

argparse 라이브러리를 사용하는 Python 모듈이 있습니다. 코드베이스의 해당 섹션에 대한 테스트를 작성하려면 어떻게합니까?


argparse는 명령 행 인터페이스입니다. 명령 행을 통해 애플리케이션을 호출하기위한 테스트를 작성하십시오.
Homer6

귀하의 질문은 어려운 이해할 수 있습니다 무엇을 테스트 할. 예를 들어 "명령 줄 인수 X, Y, Z를 사용하면 함수 foo()가 호출됩니다"라고 생각합니다. sys.argv그렇다면 조롱이 해답입니다. cli-test-helpers 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
답변 주셔서 감사합니다. 특정 인수가 전달되지 않은 경우 오류를 어떻게 테스트합니까?
Pratik Khadloya

3
@PratikKhadloya 인수가 필요하지만 전달되지 않으면 argparse는 예외를 발생시킵니다.
Viktor Kerkez

2
@PratikKhadloya 예는 메시지가 불행하게도 정말 도움이되지 않습니다 :( 그것은 단지 2... argparse그것은 직접 인쇄하기 때문에 매우 친절 테스트되지 sys.stderr...
빅토르 Kerkez

1
@ViktorKerkez mock.assert_called_with 또는 mock_calls를 검사하여 특정 메시지를 확인하기 위해 sys.stderr를 조롱 할 수 있습니다 . 자세한 내용 은 docs.python.org/3/library/unittest.mock.html 을 참조하십시오 . stdin 조롱의 예 는 stackoverflow.com/questions/6271947/… 도 참조하십시오 . (stderr는 비슷해야 함)
BryCoBat

1
@PratikKhadloya는 오류 처리 / 테스트 오류에 대한 내 답변을 참조하십시오 stackoverflow.com/a/55234595/1240268
Andy Hayden

25

"argparse 부분"은 약간 모호하므로이 답변은 한 부분, 즉 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. ( docs 참조 )이 스타일은 각 메소드 인수에 대해 다른 값이 전달되는 경우를 신속하게 테스트하는 데 유용합니다. Namespace테스트에서 전체 argparse 비 의존성을 위해 조롱 하기로 선택한 경우 실제 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())

그러나 이제 단위 테스트 코드 argparseNamespace클래스 에 따라 다릅니다 . 당신은 조롱해야합니다 Namespace.
imrek 2016 년

1
으스스한 음색에 대한 @DrunkenMaster 사과. 설명과 가능한 용도로 답변을 업데이트했습니다. 나는 여기서도 배우고 있으므로, 당신이 원한다면, 당신 (또는 다른 사람)이 반환 가치를 조롱하는 것이 유익한 경우를 제공 할 수 있습니까? (또는 최소한의 경우에 없는 반환 값을 조롱 해로운)
문수는

1
from unittest import mock이제 올바른 수입 방법입니다-적어도 python3에 적합
Michael Hall

1
@MichaelHall 감사합니다. 스 니펫을 업데이트하고 상황에 맞는 정보를 추가했습니다.
munsu

1
여기서 Namespace수업을 사용 하는 것은 내가 찾던 것입니다. 테스트에 여전히 의지하고 있음에도 불구하고 테스트중인 코드 argparse의 특정 구현에 의존하지 않으므로 argparse단위 테스트에 중요합니다. 또한 pytestparametrize()메소드 를 사용 하여을 포함하는 템플릿 모의를 사용하여 다양한 인수 조합을 빠르게 테스트 할 수 있습니다 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. 플래그와 덤프 args 플래그를 사용하여 배치 / 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


5

parse_args를 던지고 SystemExitstderr에 인쇄하면 다음 두 가지를 모두 잡을 수 있습니다.

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 == "?"
    ...
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.