J'ai un programme de console écrit en Python. Il pose des questions à l'utilisateur à l'aide de la commande:
some_input = input('Answer the question:', ...)
Comment tester une fonction contenant un appel à input
en utilisant pytest
? Je ne voudrais pas forcer un testeur à saisir du texte plusieurs fois seulement pour terminer un test.
Vous devriez probablement vous moquer de la fonction intégrée input
, vous pouvez utiliser la fonctionnalité teardown
fournie par pytest
pour revenir à la fonction input
d'origine après chaque test.
import module # The module which contains the call to input
class TestClass:
def test_function_1(self):
# Override the Python built-in input method
module.input = lambda: 'some_input'
# Call the function you would like to test (which uses input)
output = module.function()
assert output == 'expected_output'
def test_function_2(self):
module.input = lambda: 'some_other_input'
output = module.function()
assert output == 'another_expected_output'
def teardown_method(self, method):
# This method is being called after each test case, and it will revert input back to original function
module.input = input
Une solution plus élégante consisterait à utiliser le module mock
avec un with statement
. De cette façon, vous n'avez pas besoin d'utiliser le démontage et la méthode corrigée ne vivra que dans la portée with
.
import mock
import module
def test_function():
with mock.patch.object(__builtins__, 'input', lambda: 'some_input'):
assert module.function() == 'expected_output'
Comme l'a suggéré le compilateur, pytest a un nouveau montage monkeypatch pour cela. Un objet monkeypatch peut modifier un attribut dans une classe ou une valeur dans un dictionnaire, puis restaurer sa valeur d'origine à la fin du test.
Dans ce cas, la fonction intégrée input
est une valeur du dictionnaire __builtins__
De python, nous pouvons donc la modifier comme suit:
def test_something_that_involves_user_input(monkeypatch):
# monkeypatch the "input" function, so that it returns "Mark".
# This simulates the user entering "Mark" in the terminal:
monkeypatch.setattr('builtins.input', lambda: "Mark")
# go about using input() like you normally would:
i = input("What is your name?")
assert i == "Mark"
Vous pouvez remplacer sys.stdin
par des personnalisations Text IO , comme l'entrée d'un fichier ou d'un tampon StringIO en mémoire:
import sys
class Test:
def test_function(self):
sys.stdin = open("preprogrammed_inputs.txt")
module.call_function()
def setup_method(self):
self.orig_stdin = sys.stdin
def teardown_method(self):
sys.stdin = self.orig_stdin
c'est plus robuste que de simplement patcher input()
, car cela ne sera pas suffisant si le module utilise d'autres méthodes de consommation de texte depuis stdin.
Cela peut également être fait avec élégance avec un gestionnaire de contexte personnalisé
import sys
from contextlib import contextmanager
@contextmanager
def replace_stdin(target):
orig = sys.stdin
sys.stdin = target
yield
sys.stdin = orig
Et puis il suffit de l'utiliser comme ceci par exemple:
with replace_stdin(StringIO("some preprogrammed input")):
module.call_function()
Vous pouvez le faire avec mock.patch
comme suit.
Tout d'abord, dans votre code, créez une fonction factice pour les appels à input
:
def __get_input(text):
return input(text)
Dans vos fonctions de test:
import my_module
from mock import patch
@patch('my_module.__get_input', return_value='y')
def test_what_happens_when_answering_yes(self, mock):
"""
Test what happens when user input is 'y'
"""
# whatever your test function does
Par exemple, si vous avez une boucle vérifiant que les seules réponses valides sont dans ['y', 'Y', 'n', 'N'], vous pouvez tester que rien ne se produit lorsque vous entrez une valeur différente à la place.
Dans ce cas, nous supposons qu'un
SystemExit
est levé lorsque vous répondez 'N':
@patch('my_module.__get_input')
def test_invalid_answer_remains_in_loop(self, mock):
"""
Test nothing's broken when answer is not ['Y', 'y', 'N', 'n']
"""
with self.assertRaises(SystemExit):
mock.side_effect = ['k', 'l', 'yeah', 'N']
# call to our function asking for input
Cela peut être fait avec les blocs mock.patch
Et with
en python3.
import pytest
import mock
import builtins
"""
The function to test (would usually be loaded
from a module outside this file).
"""
def user_Prompt():
ans = input('Enter a number: ')
try:
float(ans)
except:
import sys
sys.exit('NaN')
return 'Your number is {}'.format(ans)
"""
This test will mock input of '19'
"""
def test_user_Prompt_ok():
with mock.patch.object(builtins, 'input', lambda _: '19'):
assert user_Prompt() == 'Your number is 19'
La ligne à noter est mock.patch.object(builtins, 'input', lambda _: '19'):
, qui remplace input
par la fonction lambda. Notre fonction lambda prend une variable jetable _
Parce que input
prend un argument.
Voici comment tester le cas d'échec, où user_input appelle sys.exit
. L'astuce ici est d'obtenir que pytest recherche cette exception avec pytest.raises(SystemExit)
.
"""
This test will mock input of 'nineteen'
"""
def test_user_Prompt_exit():
with mock.patch.object(builtins, 'input', lambda _: 'nineteen'):
with pytest.raises(SystemExit):
user_Prompt()
Vous devriez pouvoir exécuter ce test en copiant et collant le code ci-dessus dans un fichier tests/test_.py
Et en exécutant pytest
à partir du répertoire parent.