Làm thế nào để bạn viết bài kiểm tra cho phần argparse của một mô-đun python? [đóng cửa]


162

Tôi có một mô-đun Python sử dụng thư viện argparse. Làm cách nào để viết bài kiểm tra cho phần đó của cơ sở mã?


argparse là một giao diện dòng lệnh. Viết bài kiểm tra của bạn để gọi ứng dụng thông qua dòng lệnh.
Homer6

Câu hỏi của bạn làm cho khó hiểu những gì bạn muốn kiểm tra. Tôi nghi ngờ đó là cuối cùng, ví dụ: "khi tôi sử dụng các đối số dòng lệnh X, Y, Z thì hàm foo()được gọi". Mocking of sys.argvlà câu trả lời nếu đó là trường hợp. Hãy xem gói Python cli-test-helpers . Xem thêm stackoverflow.com/a/58594599/202834
Peterino 16/11/19

Câu trả lời:


214

Bạn nên cấu trúc lại mã của mình và di chuyển phân tích cú pháp sang một hàm:

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

Sau đó, trong mainchức năng của bạn, bạn chỉ cần gọi nó với:

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

(trong đó phần tử đầu tiên sys.argvđại diện cho tên tập lệnh được loại bỏ để không gửi nó dưới dạng một công tắc bổ sung trong quá trình hoạt động CLI.)

Trong các thử nghiệm của bạn, sau đó bạn có thể gọi hàm phân tích cú pháp với bất kỳ danh sách đối số nào bạn muốn kiểm tra nó với:

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

Bằng cách này, bạn sẽ không bao giờ phải thực thi mã ứng dụng của mình chỉ để kiểm tra trình phân tích cú pháp.

Nếu bạn cần thay đổi và / hoặc thêm tùy chọn vào trình phân tích cú pháp sau này trong ứng dụng của mình, thì hãy tạo phương thức xuất xưởng:

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

Sau này bạn có thể thao tác nó nếu bạn muốn và một bài kiểm tra có thể như sau:

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
Cảm ơn câu trả lời của bạn. Làm cách nào để kiểm tra lỗi khi một đối số nhất định không được thông qua?
Pratik Khadloya

3
@PratikKhadloya Nếu đối số là bắt buộc và nó không được thông qua, argparse sẽ đưa ra một ngoại lệ.
Viktor Kerkez 5/2/2015

2
@PratikKhadloya Vâng, thông báo rất tiếc là không thực sự hữu ích :( Chỉ là 2... argparsenó không thân thiện lắm vì nó được in trực tiếp tới sys.stderr...
Viktor Kerkez 6/215

1
@ViktorKerkez Bạn có thể sys.stderr giả để kiểm tra một thông điệp cụ thể, hoặc mock.assert_called_with hoặc bằng cách kiểm tra mock_calls, xem docs.python.org/3/library/unittest.mock.html để xem chi tiết hơn. Xem thêm stackoverflow.com/questions/6271947/ khăn để biết ví dụ về chế nhạo stdin. (stderr nên tương tự)
BryCoBat

1
@PratikKhadloya xem câu trả lời của tôi để xử lý / kiểm tra lỗi stackoverflow.com/a/55234595/1240268
Andy Hayden

25

"Phần argparse" hơi mơ hồ nên câu trả lời này tập trung vào một phần: parse_argsphương pháp. Đây là phương thức tương tác với dòng lệnh của bạn và nhận được tất cả các giá trị được truyền. Về cơ bản, bạn có thể giả định những gì parse_argstrả về để không thực sự cần lấy giá trị từ dòng lệnh. Các mock gói có thể được cài đặt thông qua pip cho các phiên bản python 2,6-3,2. Đó là một phần của thư viện tiêu chuẩn kể unittest.mocktừ phiên bản 3.3 trở đi.

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

Bạn phải bao gồm tất cả các đối số của phương thức lệnh của bạn Namespace ngay cả khi chúng không được thông qua. Cung cấp cho các đối số một giá trị của None. (xem tài liệu ) Kiểu này hữu ích để nhanh chóng thực hiện kiểm tra cho các trường hợp trong đó các giá trị khác nhau được truyền cho mỗi đối số phương thức. Nếu bạn chọn Namespacetự chế giễu bản thân để hoàn toàn không phụ thuộc vào các thử nghiệm của mình, hãy đảm bảo rằng nó hoạt động tương tự như Namespacelớp thực tế .

Dưới đây là một ví dụ sử dụng đoạn mã đầu tiên từ thư viện 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())

Nhưng bây giờ mã không đáng tin cậy nhất của bạn cũng phụ thuộc vào argparseNamespacelớp của nó . Bạn nên chế giễu Namespace.
imrek

1
@DrunkenMaster xin lỗi vì giai điệu lén lút. Tôi cập nhật câu trả lời của tôi với lời giải thích và sử dụng có thể. Tôi cũng đang học ở đây vì vậy nếu bạn muốn, bạn có thể (hoặc ai đó) cung cấp các trường hợp mà việc chế giễu giá trị trả về có lợi không? (hoặc ít nhất là các trường hợp không chế nhạo giá trị trả lại là bất lợi)
munsu

1
from unittest import mockbây giờ là phương thức nhập chính xác - ít nhất là cho python3
Michael Hall

1
@MichaelHall cảm ơn. Tôi đã cập nhật đoạn trích và thêm thông tin theo ngữ cảnh.
munsu

1
Việc sử dụng các Namespacelớp học ở đây là chính xác những gì tôi đang tìm kiếm. Mặc dù thử nghiệm vẫn dựa vào argparse, nhưng nó không dựa vào việc triển khai cụ thể argparsetheo mã được thử nghiệm, điều này rất quan trọng đối với các thử nghiệm đơn vị của tôi. Ngoài ra, thật dễ dàng để sử dụng phương pháp pytestcủa nó parametrize()để nhanh chóng kiểm tra các kết hợp đối số khác nhau với một mô hình giả định bao gồm return_value=argparse.Namespace(accumulate=accumulate, integers=integers).
acetone

17

Làm cho main()hàm của bạn lấy argvlàm đối số thay vì để nó đọc sys.argvtheo mặc định :

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

Sau đó, bạn có thể kiểm tra bình thường.

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. Điền danh sách arg của bạn bằng cách sử dụng sys.argv.append()và sau đó gọi parse(), kiểm tra kết quả và lặp lại.
  2. Gọi từ một tệp bó / bash với cờ của bạn và cờ kết xuất.
  3. Đặt tất cả phân tích đối số của bạn vào một tệp riêng biệt và trong if __name__ == "__main__":phân tích cuộc gọi và kết xuất / đánh giá kết quả sau đó kiểm tra điều này từ một tệp bó / bash.

9

Tôi không muốn sửa đổi tập lệnh phục vụ ban đầu nên tôi chỉ chế giễu sys.argvphần đó trong argparse.

from unittest.mock import patch

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

Điều này phá vỡ nếu triển khai argparse thay đổi nhưng đủ cho một kịch bản thử nghiệm nhanh. Độ nhạy là quan trọng hơn nhiều so với tính đặc hiệu trong các kịch bản thử nghiệm dù sao đi nữa.


6

Một cách đơn giản để kiểm tra trình phân tích cú pháp là:

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

Một cách khác là sửa đổi sys.argvvà gọiargs = parser.parse_args()

Có rất nhiều ví dụ về thử nghiệm argparsetronglib/test/test_argparse.py


5

parse_argsném a SystemExitvà in ra stderr, bạn có thể bắt cả hai thứ sau:

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

Bạn kiểm tra stderr (sử dụng err.seek(0); err.read()nhưng nói chung là độ chi tiết không bắt buộc.

Bây giờ bạn có thể sử dụng assertTruehoặc bất cứ thử nghiệm nào bạn thích:

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

Ngoài ra, bạn có thể muốn bắt và suy nghĩ lại một lỗi khác (thay vì 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

Khi truyền kết quả từ argparse.ArgumentParser.parse_argsmột hàm, đôi khi tôi sử dụng một namedtupleđối số để giả định để kiểm tra.

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

Để kiểm tra CLI (giao diện dòng lệnh) và không phải đầu ra lệnh, tôi đã làm một cái gì đó như thế này

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 == "?"
    ...
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.