web-dev-qa-db-de.com

Wie teste ich eine Funktion mit Eingabeaufruf?

Ich habe ein Konsolenprogramm in Python geschrieben. Es stellt den Benutzern Fragen mit dem folgenden Befehl:

some_input = input('Answer the question:', ...)

Wie würde ich eine Funktion testen, die einen Aufruf von input mit pytest enthält? Ich möchte einen Tester nicht zwingen, mehrmals Text einzugeben, um nur einen Testlauf zu beenden.

35

Sie sollten wahrscheinlich die integrierte input -Funktion verspotten. Sie können die teardown -Funktion verwenden, die von pytest um nach jedem Test zur ursprünglichen input Funktion zurückzukehren.

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  

Eine elegantere Lösung wäre die Verwendung des Moduls mock zusammen mit einem with statement . Auf diese Weise müssen Sie keine Teardown-Methode verwenden, und die gepatchte Methode wird nur innerhalb des Bereichs with ausgeführt.

import mock
import module

def test_function():
    with mock.patch.object(__builtins__, 'input', lambda: 'some_input'):
        assert module.function() == 'expected_output'
16
Forge

Wie der Compiler vorschlug, hat pytest dafür ein neues Monkeypatch-Fixture. Ein Monkeypatch -Objekt kann ein Attribut in einer Klasse oder einen Wert in einem Dictionary ändern und dann am Ende des Tests seinen ursprünglichen Wert wiederherstellen.

In diesem Fall ist die eingebaute Funktion input ein Wert von Pythons __builtins__ Wörterbuch, so können wir es so ändern:

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

Sie können sys.stdin durch ein benutzerdefiniertes Text IO ersetzen, z. B. durch Eingaben aus einer Datei oder einem speicherinternen StringIO-Puffer:

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

dies ist robuster als nur das Patchen von input(), da dies nicht ausreicht, wenn das Modul andere Methoden zum Verarbeiten von Text aus stdin verwendet.

Dies kann auch sehr elegant mit einem benutzerdefinierten Kontextmanager durchgeführt werden

import sys
from contextlib import contextmanager

@contextmanager
def replace_stdin(target):
    orig = sys.stdin
    sys.stdin = target
    yield
    sys.stdin = orig

Und dann benutze es einfach so, zum Beispiel:

with replace_stdin(StringIO("some preprogrammed input")):
    module.call_function()
16
Felk

Du kannst es mit mock.patch wie folgt.

Erstellen Sie zunächst in Ihrem Code eine Dummy-Funktion für die Aufrufe von input:

def __get_input(text):
    return input(text)

In Ihren Testfunktionen:

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

Wenn Sie beispielsweise in einer Schleife prüfen, ob die einzig gültigen Antworten in ['y', 'Y', 'n', 'N'] enthalten sind, können Sie testen, ob nichts passiert, wenn Sie stattdessen einen anderen Wert eingeben.

In diesem Fall nehmen wir an, dass ein SystemExit ausgelöst wird, wenn Sie mit 'N' antworten:

@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

Dies kann mit den Blöcken mock.patch Und with in python3 geschehen.

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'

Die zu notierende Zeile ist mock.patch.object(builtins, 'input', lambda _: '19'):, die die input mit der Lambda-Funktion überschreibt. Unsere Lambda-Funktion nimmt eine Wegwerfvariable _ Auf, weil input ein Argument aufnimmt.

So können Sie den Fail-Fall testen, bei dem user_input sys.exit Aufruft. Der Trick hier ist, mit pytest.raises(SystemExit) nach dieser Ausnahme zu suchen.

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

Sie sollten in der Lage sein, diesen Test zum Laufen zu bringen, indem Sie den obigen Code kopieren und in eine Datei tests/test_.py Einfügen und pytest aus dem übergeordneten Verzeichnis ausführen.

2
AlexG