python spot met onbewerkte invoer in unittests

Stel dat ik deze pythoncode heb:

def answer():
    ans = raw_input('enter yes or no')
    if ans == 'yes':
        print 'you entered yes'
    if ans == 'no':
        print 'you entered no'

Hoe schrijf ik hiervoor een unittest? Ik weet dat ik ‘Mock’ moet gebruiken, maar ik begrijp niet hoe. Kan iemand een eenvoudig voorbeeld geven?


Antwoord 1, autoriteit 100%

Je kunt invoer niet patchen, maar je kunt het wel verpakken om mock.patch() te gebruiken. Hier is een oplossing:

from unittest.mock import patch
from unittest import TestCase
def get_input(text):
    return input(text)
def answer():
    ans = get_input('enter yes or no')
    if ans == 'yes':
        return 'you entered yes'
    if ans == 'no':
        return 'you entered no'
class Test(TestCase):
    # get_input will return 'yes' during this test
    @patch('yourmodule.get_input', return_value='yes')
    def test_answer_yes(self, input):
        self.assertEqual(answer(), 'you entered yes')
    @patch('yourmodule.get_input', return_value='no')
    def test_answer_no(self, input):
        self.assertEqual(answer(), 'you entered no')

Houd er rekening mee dat dit fragment alleen werkt in Python-versies 3.3+


Antwoord 2, autoriteit 54%

Ok, ten eerste vind ik het nodig om erop te wijzen dat er in de oorspronkelijke code in kwestie twee dingen zijn die moeten worden aangepakt:

  1. raw_input(een input-neveneffect) moet worden bespot.
  2. print(een neveneffect van de uitvoer) moet worden aangevinkt.

In een ideale functie voor unit testing zouden er geen bijwerkingen zijn. Een functie zou eenvoudig worden getest door argumenten in te dienen en de uitvoer ervan zou worden gecontroleerd. Maar vaak willen we functies testen die niet ideaal zijn, IE, in functies zoals die van jou.

Dus wat moeten we doen? Welnu, in Python 3.3 werden beide problemen die ik hierboven noemde triviaal omdat de module unittestde mogelijkheid kreeg om te spotten en te controleren op bijwerkingen. Maar vanaf het begin van 2014 was slechts 30% van de Python-programmeurs overgestapt op 3.x, dus omwille van de andere 70% van de Python-programmeurs die nog steeds 2.x gebruiken, zal ik een antwoord schetsen. In het huidige tempo zal 3.x de 2.x pas in ~2019 inhalen en 2.x pas in ~2027 verdwijnen. Dus ik denk dat dit antwoord de komende jaren nuttig zal zijn.

Ik wil de hierboven genoemde problemen één voor één aanpakken, dus ik ga in eerste instantie uw functie wijzigen van het gebruik van printals uitvoer naar het gebruik van return. Geen verrassingen, hier is die code:

def answerReturn():
    ans = raw_input('enter yes or no')
    if ans == 'yes':
        return 'you entered yes'
    if ans == 'no':
        return 'you entered no'

We hoeven dus alleen maar raw_inputte spotten. Makkelijk genoeg – Omid Raha’s antwoord op deze vraaglaat ons zien hoe we dat kunnen doen door de __builtins__.raw_inputimplementatie met onze schijnimplementatie. Alleen was zijn antwoord niet goed georganiseerd in een TestCaseen functies, dus ik zal dat demonstreren.

import unittest    
class TestAnswerReturn(unittest.TestCase):
    def testYes(self):
        original_raw_input = __builtins__.raw_input
        __builtins__.raw_input = lambda _: 'yes'
        self.assertEqual(answerReturn(), 'you entered yes')
        __builtins__.raw_input = original_raw_input
    def testNo(self):
        original_raw_input = __builtins__.raw_input
        __builtins__.raw_input = lambda _: 'no'
        self.assertEqual(answerReturn(), 'you entered no')
        __builtins__.raw_input = original_raw_input

Kleine opmerking alleen over de naamgevingsconventies van Python – variabelen die door de parser worden vereist maar niet worden gebruikt, worden meestal _genoemd, zoals in het geval van de ongebruikte variabele van de lambda (die normaal gesproken de prompt is die wordt getoond aan de gebruiker in het geval van de raw_input, voor het geval je je afvraagt ​​waarom het in dit geval nodig is).

Hoe dan ook, dit is rommelig en overbodig. Dus ik ga de herhaling afschaffen door een contextmanagertoe te voegen, die eenvoudige with-statements mogelijk maakt.

from contextlib import contextmanager
@contextmanager
def mockRawInput(mock):
    original_raw_input = __builtins__.raw_input
    __builtins__.raw_input = lambda _: mock
    yield
    __builtins__.raw_input = original_raw_input
class TestAnswerReturn(unittest.TestCase):
    def testYes(self):
        with mockRawInput('yes'):
            self.assertEqual(answerReturn(), 'you entered yes')
    def testNo(self):
        with mockRawInput('no'):
            self.assertEqual(answerReturn(), 'you entered no')

Ik denk dat dit het eerste deel hiervan mooi beantwoordt. Op naar het tweede deel – printcontroleren. Ik vond dit veel lastiger – ik hoor graag of iemand een beter antwoord heeft.

Hoe dan ook, de print-instructie kan niet worden overschreven, maar als u in plaats daarvan print()-functies gebruikt (zoals u zou moeten) en from __future__ import print_functionje kunt het volgende gebruiken:

class PromiseString(str):
    def set(self, newString):
        self.innerString = newString
    def __eq__(self, other):
        return self.innerString == other
@contextmanager
def getPrint():
    promise = PromiseString()
    original_print = __builtin__.print
    __builtin__.print = lambda message: promise.set(message)
    yield promise
    __builtin__.print = original_print
class TestAnswer(unittest.TestCase):
    def testYes(self):
        with mockRawInput('yes'), getPrint() as response:
            answer()
            self.assertEqual(response, 'you entered yes')
    def testNo(self):
        with mockRawInput('no'), getPrint() as response:
            answer()
            self.assertEqual(response, 'you entered no')

Het lastige hier is dat je een antwoord moet yieldvoordat het blok withwordt ingevoerd. Maar je kunt niet weten wat dat antwoord zal zijn totdat het print()in het with-blok wordt aangeroepen. Dit zou prima zijn als strings veranderlijk waren, maar dat zijn ze niet. Dus in plaats daarvan werd een kleine belofte of proxy-klasse gedaan – PromiseString. Het doet maar twee dingen: laat een string (of wat dan ook) instellen en laat ons weten of deze gelijk is aan een andere string. Een PromiseStringwordt yielded en vervolgens ingesteld op de waarde die normaal gesproken printzou zijn binnen het with-blok.

Hopelijk waardeer je al deze bedrog die ik heb opgeschreven, aangezien het me ongeveer 90 minuten kostte om deze avond in elkaar te zetten. Ik heb al deze code getest en geverifieerd dat het allemaal werkte met Python 2.7.


Antwoord 3, autoriteit 23%

Ik gebruik Python 3.4 en moest bovenstaande antwoorden aanpassen. Mijn oplossing houdt rekening met gemeenschappelijke code in de aangepaste runTest-methode en laat zien hoe u zowel input()als print()kunt patchen. Hier is de code die werkt zoals geadverteerd:

import unittest
from io import StringIO
from unittest.mock import patch
def answer():
    ans = input('enter yes or no')
    if ans == 'yes':
        print('you entered yes')
    if ans == 'no':
        print('you entered no')
class MyTestCase(unittest.TestCase):
    def runTest(self, given_answer, expected_out):
        with patch('builtins.input', return_value=given_answer), patch('sys.stdout', new=StringIO()) as fake_out:
            answer()
            self.assertEqual(fake_out.getvalue().strip(), expected_out)
    def testNo(self):
        self.runTest('no', 'you entered no')
    def testYes(self):
        self.runTest('yes', 'you entered yes')
if __name__ == '__main__':
    unittest.main()

Antwoord 4, autoriteit 13%

Ik kwam net hetzelfde probleem tegen, maar ik bespotte __builtin__.raw_input.

Alleen getest op Python 2. pip install mockals je het pakket nog niet hebt geïnstalleerd.

from mock import patch
from unittest import TestCase
class TestAnswer(TestCase):
    def test_yes(self):
        with patch('__builtin__.raw_input', return_value='yes') as _raw_input:
            self.assertEqual(answer(), 'you entered yes')
            _raw_input.assert_called_once_with('enter yes or no')
    def test_no(self):
        with patch('__builtin__.raw_input', return_value='no') as _raw_input:
            self.assertEqual(answer(), 'you entered no')
            _raw_input.assert_called_once_with('enter yes or no')

Als alternatief kunt u de twee tests vereenvoudigen door de bibliotheek gentyte gebruiken:

from genty import genty, genty_dataset
from mock import patch
from unittest import TestCase
@genty
class TestAnswer(TestCase):
    @genty_dataset(
        ('yes', 'you entered yes'),
        ('no', 'you entered no'),
    )
    def test_answer(self, expected_input, expected_answer):
        with patch('__builtin__.raw_input', return_value=expected_input) as _raw_input:
            self.assertEqual(answer(), expected_answer)
            _raw_input.assert_called_once_with('enter yes or no')

Antwoord 5, autoriteit 5%

Dit is wat ik doe in Python 3:

class MockInputFunction:
    def __init__(self, return_value=None):
        self.return_value = return_value
        self._orig_input_fn = __builtins__['input']
    def _mock_input_fn(self, prompt):
        print(prompt + str(self.return_value))
        return self.return_value
    def __enter__(self):
        __builtins__['input'] = self._mock_input_fn
    def __exit__(self, type, value, traceback):
        __builtins__['input'] = self._orig_input_fn

die vervolgens in elke context kan worden gebruikt. pytestgebruikt bijvoorbeeld gewone assert-statements.

def func():
    """ function to test """
    x = input("What is x? ")
    return int(x)
# to test, you could simply do:
with MockInputFunction(return_value=13):
    assert func() == 13

Antwoord 6

def answer():
    ans = raw_input('enter yes or no')
    if ans == 'yes':
        return 'you entered yes'
    if ans == 'no':
        return 'you entered no'
def test_answer_yes():
    assert(answer() == 'you entered yes')
def test_answer_no():
    assert(answer() == 'you entered no')
origin_raw_input = __builtins__.raw_input
__builtins__.raw_input = lambda x: "yes"
test_answer_yes()
__builtins__.raw_input = lambda x: "no"
test_answer_no()
__builtins__.raw_input = origin_raw_input

Other episodes