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?
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')
"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 mock
package 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())
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')
sys.argv.append()
, puis appelez parse()
, vérifiez les résultats et répétez l'opération.if __== "__main__":
appelez analyse et exportez/évaluez les résultats, puis testez-le à partir d'un fichier batch/bash.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.
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
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()
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())