web-dev-qa-db-fra.com

Comment écrivez-vous des tests pour la partie argparse d'un module python?

J'ai un module Python qui utilise la bibliothèque argparse. Comment puis-je écrire des tests pour cette section de la base de code?

126
pydanny

Vous devriez refactoriser votre code et déplacer l'analyse vers une fonction:

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

Ensuite, dans votre fonction main, vous devriez simplement l'appeler avec:

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

(où le premier élément de sys.argv qui représente le nom du script est supprimé pour ne pas l'envoyer en tant que commutateur supplémentaire lors de l'opération CLI.)

Dans vos tests, vous pouvez ensuite appeler la fonction d'analyse avec la liste d'arguments avec laquelle vous souhaitez le tester:

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

De cette façon, vous ne devrez jamais exécuter le code de votre application simplement pour tester l'analyseur.

Si vous devez modifier et/ou ajouter des options à votre analyseur ultérieurement dans votre application, créez une méthode de fabrique:

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

Vous pouvez le manipuler ultérieurement si vous le souhaitez, et un test pourrait ressembler à:

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')
169
Viktor Kerkez

"argparse portion" est un peu vague et cette réponse se concentre sur une partie: le parse_args méthode. C'est la méthode qui interagit avec votre ligne de commande et obtient toutes les valeurs transmises. Fondamentalement, vous pouvez vous moquer de ce que parse_args retourne afin qu'il ne soit pas nécessaire d'obtenir les valeurs de la ligne de commande. Le mockpackage peut être installé via pip pour python versions 2.6-3.2. Il fait partie de la bibliothèque standard en tant que unittest.mock à partir de la version 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

Vous devez inclure tous les arguments de votre méthode de commande dans Namespace même s'ils ne sont pas passés. Donnez à ces arguments une valeur de None. (voir le docs ) Ce style est utile pour tester rapidement des cas dans lesquels différentes valeurs sont passées pour chaque argument de méthode. Si vous choisissez de vous moquer de Namespace pour une non-confiance totale dans vos tests, assurez-vous que son comportement est similaire à celui de la classe Namespace.

Vous trouverez ci-dessous un exemple utilisant le premier extrait de la bibliothèque 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 __== "__main__":
    print(main())
19
munsu

Faites que votre fonction main() prenne argv comme argument plutôt que de la laisser lire dans sys.argv comme il le fera par défaut :

# 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 __== "__main__":
    sys.exit(main(sys.argv[1:]))

Ensuite, vous pouvez tester normalement.

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')
11
Ceasar Bautista
  1. Remplissez votre liste d'arguments en utilisant sys.argv.append(), puis appelez parse(), vérifiez les résultats et répétez l'opération.
  2. Appelez depuis un fichier batch/bash avec vos drapeaux et un drapeau de vidage des arguments.
  3. Placez tous vos arguments dans un fichier séparé et dans le fichier if __== "__main__": appelez analyse et exportez/évaluez les résultats, puis testez-le à partir d'un fichier batch/bash.
6
Steve Barnes

Je ne voulais pas modifier le script de service original, alors je me suis simplement moqué du sys.argv part dans argparse.

from unittest.mock import patch

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

Cela se produit si l'implémentation argparse change, mais suffisamment pour un script de test rapide. De toute façon, la sensibilité est beaucoup plus importante que la spécificité dans les scripts de test.

5
김민준

Un moyen simple de tester un analyseur syntaxique est le suivant:

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

Une autre méthode consiste à modifier sys.argv Et à appeler args = parser.parse_args()

Il existe de nombreux exemples de tests argparse dans lib/test/test_argparse.py

4
hpaulj

En passant résulte de argparse.ArgumentParser.parse_args à une fonction, j’utilise parfois un namedtuple pour simuler des arguments de test.

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 __== '__main__':
    unittest.main()
2
guest

parse_args Jette un SystemExit et imprime vers stderr, vous pouvez attraper ces deux choses:

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

Vous inspectez stderr (avec err.seek(0); err.read()), mais en général, cette granularité n'est pas obligatoire.

Maintenant, vous pouvez utiliser assertTrue ou celui que vous aimez:

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

Sinon, vous voudrez peut-être attraper et rediffuser une erreur différente (au lieu de 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())
1
Andy Hayden