Bir python modülünün argparse kısmı için testleri nasıl yazıyorsunuz? [kapalı]


162

Argparse kütüphanesini kullanan bir Python modülü var. Kod tabanının bu bölümü için testleri nasıl yazarım?


argparse bir komut satırı arabirimidir. Komut satırı üzerinden uygulamayı çağırmak için testlerinizi yazın.
Homer6

Sorunuz neyi test etmek istediğinizi anlamayı zorlaştırıyor . Sonuçta, örneğin "X, Y, Z komut satırı argümanlarını kullandığımda fonksiyon foo()çağrılır" olduğundan şüphelenirim . Alay etmek, sys.argveğer böyle ise cevaptır. Cli-test yardımcıları Python paketine bir göz atın . Ayrıca bkz. Stackoverflow.com/a/58594599/202834
Peterino

Yanıtlar:


214

Kodunuzu yeniden düzenlemeli ve çözümlemeyi bir işleve taşımalısınız:

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

O zaman mainfonksiyonunuzda sadece şununla çağırmalısınız:

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

(burada sys.argvkomut dosyasının adını temsil eden ilk öğe, CLI işlemi sırasında ek anahtar olarak gönderilmeyecek şekilde kaldırılır.)

Testlerinizde, ayrıştırıcı işlevini test etmek istediğiniz bağımsız değişken listesiyle çağırabilirsiniz:

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

Bu şekilde, yalnızca ayrıştırıcıyı test etmek için uygulamanızın kodunu yürütmek zorunda kalmazsınız.

Daha sonra uygulamanızda ayrıştırıcıya değişiklik yapmanız ve / veya seçenekleri eklemeniz gerekiyorsa, bir fabrika yöntemi oluşturun:

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

Daha sonra isterseniz manipüle edebilirsiniz ve bir test şöyle görünebilir:

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
Cevabınız için teşekkürler. Belirli bir argüman iletilmediğinde hataları nasıl test edebiliriz?
Pratik Khadloya

3
@PratikKhadloya Eğer argüman gerekliyse ve geçilmezse, argparse bir istisna doğuracaktır.
Viktor Kerkez

2
@PratikKhadloya Evet, mesaj maalesef gerçekten yardımcı değil :( Sadece 2... argparsedoğrudan baskıya sys.stderr
girdiği

1
Sen, belirli bir mesaj için mock.assert_called_with veya mock_calls inceleyerek ya kontrol etmek sahte sys.stderr mümkün olabilir @ViktorKerkez, bkz docs.python.org/3/library/unittest.mock.html Daha fazla ayrıntı için. Alaycı stdin örneği için bkz. Stackoverflow.com/questions/6271947/… . (stderr benzer olmalıdır)
BryCoBat

1
@PratikKhadloya hataları işleme / test etme ile ilgili cevabımı gör stackoverflow.com/a/55234595/1240268
Andy Hayden

25

"argparse kısmı" biraz belirsiz olduğundan, bu cevap bir kısma odaklanır: parse_argsyöntem. Komut satırınızla etkileşime giren ve iletilen tüm değerleri alan yöntemdir. Temel olarak, ne parse_argsdöndürdüğünü alay edebilirsiniz, böylece komut satırından değerleri alması gerekmez. mock Paket piton sürümleri 2,6-3,2 için pip yoluyla kurulabilir. unittest.mock3.3 sürümünden itibaren standart kütüphanenin bir parçasıdır .

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

Tüm komut yönteminizin argümanlarını Namespace aktarılmasalar bile dahil etmelisiniz . Bu bağımsız değişkenlere bir değer verin None. ( belgelere bakın ) Bu stil, her yöntem bağımsız değişkeni için farklı değerlerin iletildiği durumlar için hızlı bir şekilde test yapmak için kullanışlıdır. NamespaceTestlerinizde tam argparse güvenmemesi için kendini alay etmeyi seçerseniz, bunun gerçek Namespacesınıfa benzer şekilde davrandığından emin olun .

Aşağıda, argparse kitaplığındaki ilk snippet'i kullanan bir örnek verilmiştir.

# 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())

Ama şimdi unittest kodu da bağlıdır argparseve onun Namespacesınıfı. Alay etmelisin Namespace.
imrek

1
@DrunkenMark ses tonu için özür dilerim. Cevabımı açıklama ve olası kullanımlarla güncelledim. Burada da öğreniyorum, eğer öyleyse, siz (veya bir başkası) dönüş değeri ile alay etmenin faydalı olduğu durumlar sağlayabilir misiniz? (veya en azından geri dönüş değerinin alay edilmediği durumlarda zararlıdır)
munsu

1
from unittest import mockşimdi doğru ithalat yöntemi - en azından python3 için
Michael Hall

1
@MichaelHall teşekkürler. Parçacığı güncelledim ve içeriksel bilgiler ekledim.
munsu

1
NamespaceBurada sınıfın kullanımı tam olarak aradığım şeydi. Testin hala güvenilmesine rağmen, birim testlerim için önemli olan test edilen kod tarafından argparseözel olarak uygulanmasına dayanmaz argparse. Buna ek olarak, kullanımı kolay pytest'in parametrize()hızlı içeren bir şablonu mock çeşitli bağımsız değişken kombinasyonları test etmek için bir yöntem return_value=argparse.Namespace(accumulate=accumulate, integers=integers).
aseton

17

main()İşlevinizin varsayılan olarak olduğu gibi okunmasınaargv izin vermek yerine bir argüman olarak alınmasını sağlayın :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:]))

Sonra normal olarak test edebilirsiniz.

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. Kullanarak arg listenizi doldurun sys.argv.append()ve sonra arayın parse(), sonuçları kontrol edin ve tekrarlayın.
  2. Bayraklarınızla birlikte bir toplu iş / bash dosyasından çağırın ve bir döküm args bayrağı.
  3. Tüm bağımsız değişken ayrıştırma işleminizi ayrı bir dosyaya ve if __name__ == "__main__":çağrı ayrıştırma işlemine koyun ve sonuçları döküm / değerlendirin, ardından bunu bir toplu iş / bash dosyasından test edin.

9

Orijinal sunum komut dosyasını değiştirmek istemedim, bu yüzden sadece sys.argvargparse'deki parçayı alay ettim.

from unittest.mock import patch

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

Argparse uygulaması değişirse, ancak hızlı bir test komut dosyası için yeterli olursa bu durum kesilir. Zaten test komut dosyalarında hassasiyet, özgüllükten çok daha önemlidir.


6

Bir ayrıştırıcıyı test etmenin basit bir yolu:

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

Başka bir yol, değiştirmek sys.argvve aramaktırargs = parser.parse_args()

Test örnekleri çok var argparseiçindelib/test/test_argparse.py


5

parse_argsa atar SystemExitve stderr'a yazdırır, her ikisini de yakalayabilirsiniz:

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'ı inceliyorsunuz ( err.seek(0); err.read()ancak genellikle ayrıntı düzeyi gerekli değildir.

Şimdi istediğiniz assertTrueveya hangi testi istediğinizi kullanabilirsiniz:

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

Alternatif olarak (yerine SystemExit) farklı bir hatayı yakalamak ve yeniden yazmak isteyebilirsiniz :

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

Sonuçları argparse.ArgumentParser.parse_argsbir işleve namedtupleaktarırken bazen test için argümanları taklit etmek için a kullanırım.

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'yi (komut satırı arayüzü) test etmek ve komut çıkışını değil , böyle bir şey yaptım

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 == "?"
    ...
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.