web-dev-qa-db-fra.com

Comment tester une fonction avec appel d'entrée?

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.

35

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'
16
Forge

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"
29
mareoraft

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()
16
Felk

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
3
fernandezcuesta

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.

2
AlexG