ฉันมีโมดูล Python ที่ใช้ไลบรารี argparse ฉันจะเขียนการทดสอบสำหรับส่วนของรหัสฐานได้อย่างไร
foo()
เรียกใช้ฟังก์ชัน" การเยาะเย้ยของsys.argv
คือคำตอบถ้าเป็นกรณี ดูแพ็คเกจ Python ของcli-test-helpers ดูเพิ่มเติมที่stackoverflow.com/a/58594599/202834
ฉันมีโมดูล Python ที่ใช้ไลบรารี argparse ฉันจะเขียนการทดสอบสำหรับส่วนของรหัสฐานได้อย่างไร
foo()
เรียกใช้ฟังก์ชัน" การเยาะเย้ยของsys.argv
คือคำตอบถ้าเป็นกรณี ดูแพ็คเกจ Python ของcli-test-helpers ดูเพิ่มเติมที่stackoverflow.com/a/58594599/202834
คำตอบ:
คุณควร refactor รหัสของคุณและย้ายการแยกไปยังฟังก์ชั่น:
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')
2
... argparse
ไม่ได้ทดสอบง่ายมากเพราะมันพิมพ์โดยตรงไปยังsys.stderr
...
"ส่วนแย้ง" เป็นบิตคลุมเครือดังนั้นคำตอบนี้มุ่งเน้นไปที่ส่วนหนึ่ง: parse_args
วิธีการ นี่คือวิธีการโต้ตอบกับบรรทัดคำสั่งของคุณและรับค่าที่ส่งทั้งหมด โดยทั่วไปคุณสามารถเยาะเย้ยสิ่งที่parse_args
ส่งกลับเพื่อที่ว่ามันไม่จำเป็นต้องได้รับค่าจากบรรทัดคำสั่ง mock
แพคเกจสามารถติดตั้งผ่านทาง pip สำหรับรุ่นหลาม 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
คุณต้องรวม args วิธีการคำสั่งทั้งหมดของคุณไว้Namespace
แม้ว่าจะไม่ผ่านก็ตาม ให้ args 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
คุณควรจะเยาะเย้ย
from unittest import mock
ตอนนี้เป็นวิธีการนำเข้าที่ถูกต้อง - อย่างน้อยสำหรับ python3
Namespace
ชั้นเรียนที่นี่เป็นสิ่งที่ฉันกำลังมองหา แม้จะยังคงใช้การทดสอบอยู่argparse
แต่ก็ไม่ได้ขึ้นอยู่กับการใช้งานเฉพาะของargparse
รหัสภายใต้การทดสอบซึ่งเป็นสิ่งสำคัญสำหรับการทดสอบหน่วยของฉัน นอกจากนี้มันเป็นเรื่องง่ายที่จะใช้pytest
's วิธีการทดสอบได้อย่างรวดเร็วรวมกันโต้แย้งต่างๆที่มีการจำลองเทมเพลตที่มีparametrize()
return_value=argparse.Namespace(accumulate=accumulate, integers=integers)
ทำให้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')
sys.argv.append()
แล้วโทร
parse()
ตรวจสอบผลลัพธ์และทำซ้ำif __name__ == "__main__":
แยกวิเคราะห์การโทรและการถ่ายโอนข้อมูล / ประเมินผลแล้วทดสอบสิ่งนี้จากชุดไฟล์ / ทุบตีฉันไม่ต้องการแก้ไขสคริปต์การแสดงต้นฉบับดังนั้นฉันจึงเยาะเย้ยsys.argv
ส่วนในการโต้แย้ง
from unittest.mock import patch
with patch('argparse._sys.argv', ['python', 'serve.py']):
... # your test code here
สิ่งนี้จะหยุดพักหากการใช้งานแบบโต้เถียงเปลี่ยนไป แต่เพียงพอสำหรับสคริปต์ทดสอบอย่างรวดเร็ว ความรู้สึกมีความสำคัญมากกว่าความจำเพาะในสคริปต์ทดสอบต่อไป
วิธีง่ายๆในการทดสอบ parser คือ:
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
parse_args
ขว้าง a 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())
เมื่อส่งผลลัพธ์จาก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()
สำหรับการทดสอบ 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 == "?"
...