Bagaimana Anda menulis tes untuk bagian argparse dari modul python? [Tutup]

162

Saya memiliki modul Python yang menggunakan perpustakaan argparse. Bagaimana cara menulis tes untuk bagian basis kode itu?

pydanny
sumber
argparse adalah antarmuka baris perintah. Tulis tes Anda untuk menjalankan aplikasi melalui baris perintah.
Homer6
Pertanyaan Anda membuatnya sulit untuk memahami apa yang ingin Anda uji. Saya menduga itu pada akhirnya, misalnya "ketika saya menggunakan argumen baris perintah X, Y, Z maka fungsinya foo()disebut". Mengejek sys.argvadalah jawabannya jika itu masalahnya. Lihatlah paket Python cli-test-helpers . Lihat juga stackoverflow.com/a/58594599/202834
Peterino

Jawaban:

214

Anda harus memperbaiki kode Anda dan memindahkan parsing ke fungsi:

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

Maka dalam mainfungsi Anda, Anda harus menyebutnya dengan:

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

(di mana elemen pertama sys.argvyang mewakili nama skrip dihapus untuk tidak mengirimkannya sebagai saklar tambahan selama operasi CLI.)

Dalam pengujian Anda, Anda kemudian dapat memanggil fungsi parser dengan daftar argumen apa pun yang ingin Anda uji dengan:

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

Dengan cara ini Anda tidak perlu menjalankan kode aplikasi Anda hanya untuk menguji parser.

Jika Anda perlu mengubah dan / atau menambahkan opsi ke parser Anda nanti di aplikasi Anda, kemudian buat metode pabrik:

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

Anda kemudian dapat memanipulasi jika Anda mau, dan tes bisa terlihat seperti:

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')
Viktor Kerkez
sumber
4
Terima kasih atas jawaban anda. Bagaimana kami menguji kesalahan ketika argumen tertentu tidak lulus?
Pratik Khadloya
3
@PratikKhadloya Jika argumen diperlukan dan itu tidak lulus, argparse akan memunculkan pengecualian.
Viktor Kerkez
2
@PratikKhadloya Ya, pesannya sayangnya tidak terlalu membantu :( Hanya saja 2... argparsetidak terlalu ramah uji karena langsung mencetak ke sys.stderr...
Viktor Kerkez
1
@ ViktorKerkez Anda mungkin dapat mengejek sys.stderr untuk memeriksa pesan tertentu, baik mock.assert_called_with atau dengan memeriksa mock_calls, lihat docs.python.org/3/library/unittest.mock.html untuk detail lebih lanjut. Lihat juga stackoverflow.com/questions/6271947/... untuk contoh mengejek stdin. (stderr harus serupa)
BryCoBat
1
@PratikKhadloya lihat jawaban saya untuk menangani / menguji kesalahan stackoverflow.com/a/55234595/1240268
Andy Hayden
25

"Bagian argparse" agak kabur sehingga jawaban ini berfokus pada satu bagian: parse_argsmetode. Ini adalah metode yang berinteraksi dengan baris perintah Anda dan mendapatkan semua nilai yang diteruskan. Pada dasarnya, Anda bisa mengejek apa yang parse_argskembali sehingga tidak perlu benar-benar mendapatkan nilai dari baris perintah. The mock paket dapat diinstal melalui pip untuk python versi 2,6-3,2. Ini bagian dari perpustakaan standar unittest.mocksejak versi 3.3 dan seterusnya.

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

Anda harus memasukkan semua argumen metode perintah Anda Namespace bahkan jika itu tidak lulus. Beri mereka nilai args None. (lihat dokumen ) Gaya ini berguna untuk melakukan pengujian dengan cepat untuk kasus-kasus di mana nilai yang berbeda dilewatkan untuk setiap argumen metode. Jika Anda memilih untuk mengejek Namespacediri sendiri untuk ketidakpercayaan argparse total dalam tes Anda, pastikan itu berperilaku serupa dengan Namespacekelas yang sebenarnya .

Di bawah ini adalah contoh menggunakan potongan pertama dari perpustakaan 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
sumber
Tapi sekarang kode unittest Anda juga tergantung pada argparsedan Namespacekelasnya. Anda harus mengejek Namespace.
imrek
1
@DrunkenMaster meminta maaf atas nada snarky. Saya memperbarui jawaban saya dengan penjelasan dan kemungkinan penggunaan. Saya belajar di sini juga jadi jika Anda mau, bisakah Anda (atau orang lain) memberikan kasus-kasus di mana mengejek nilai pengembalian itu bermanfaat? (atau setidaknya kasus di mana tidak mengejek nilai kembali merugikan)
munsu
1
from unittest import mocksekarang merupakan metode impor yang benar - setidaknya untuk python3
Michael Hall
1
@Michael, terima kasih. Saya memperbarui cuplikan dan menambahkan info kontekstual.
munsu
1
Penggunaan Namespacekelas di sini adalah persis apa yang saya cari. Meskipun pengujian masih mengandalkan argparse, itu tidak bergantung pada implementasi tertentu argparseoleh kode yang diuji, yang penting untuk pengujian unit saya. Selain itu, mudah digunakan pytest's parametrize()metode untuk cepat menguji berbagai kombinasi argumen dengan pura-pura templated yang mencakup return_value=argparse.Namespace(accumulate=accumulate, integers=integers).
Aseton
17

Jadikan main()fungsi Anda argvsebagai argumen alih-alih membiarkannya membaca sys.argvseperti apa adanya secara default :

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

Maka Anda dapat menguji secara normal.

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')
Ceasar Bautista
sumber
9
  1. Isi daftar arg Anda dengan menggunakan sys.argv.append()lalu panggil parse(), periksa hasilnya dan ulangi.
  2. Panggil dari file batch / bash dengan flag Anda dan flag dump args.
  3. Masukkan semua argumen Anda parsing dalam file terpisah dan dalam if __name__ == "__main__":panggilan parse dan dump / evaluasi hasilnya kemudian uji ini dari file batch / bash.
Steve Barnes
sumber
9

Saya tidak ingin memodifikasi skrip penayangan asli sehingga saya hanya mengejek sys.argvbagian dalam argparse.

from unittest.mock import patch

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

Ini rusak jika implementasi argparse berubah tetapi cukup untuk skrip pengujian cepat. Sensibilitas jauh lebih penting daripada spesifisitas dalam skrip tes.

김민준
sumber
6

Cara sederhana untuk menguji parser adalah:

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

Cara lain adalah memodifikasi sys.argv , dan meneleponargs = parser.parse_args()

Ada banyak contoh pengujian argparsedilib/test/test_argparse.py

hpaulj
sumber
5

parse_argsmelempar SystemExitdan mencetak ke stderr, Anda dapat menangkap keduanya:

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

Anda memeriksa stderr (menggunakan err.seek(0); err.read() tetapi secara umum granularity tidak diperlukan.

Sekarang Anda dapat menggunakan assertTrueatau pengujian apa pun yang Anda suka:

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

Sebagai alternatif, Anda mungkin ingin menangkap dan memikirkan kembali kesalahan yang berbeda (bukan 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())
Andy Hayden
sumber
2

Saat meneruskan hasil dari argparse.ArgumentParser.parse_argske suatu fungsi, saya terkadang menggunakan a namedtupleuntuk mengejek argumen untuk pengujian.

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()
tamu
sumber
0

Untuk menguji CLI (antarmuka baris perintah), dan bukan output perintah, saya melakukan sesuatu seperti ini

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
sumber