web-dev-qa-db-fra.com

python se moquant de raw_input dans les tests unitaires

Supposons que j'ai ce code python:

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

Comment puis-je écrire un unittest pour cela? Je sais que je dois utiliser 'Mock' mais je ne comprends pas comment. Quelqu'un peut-il faire un exemple simple?

25
user3156971

Vous ne pouvez pas patcher l'entrée, mais vous pouvez l'envelopper pour utiliser mock.patch (). Voici une solution:

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')

N'oubliez pas que cet extrait ne fonctionnera que dans les versions Python 3.3+

34
gawel

D'abord, je pense qu'il est nécessaire de souligner que dans le code d'origine en question, il y a en fait deux points à aborder:

  1. raw_input (un effet secondaire lié à l'entrée) doit être simulé.
  2. print (un effet secondaire de sortie) doit être vérifié.

Dans une fonction idéale pour les tests unitaires, il n'y aurait aucun effet secondaire. Une fonction serait simplement testée en remettant des arguments et sa sortie serait vérifiée. Mais souvent, nous voulons tester des fonctions qui ne sont pas idéales, IE, dans des fonctions comme la vôtre.

Alors, que devons-nous faire? Eh bien, dans Python 3.3, les deux problèmes que j'ai énumérés ci-dessus sont devenus triviaux car le module unittest a désormais la capacité de se moquer et de rechercher les effets secondaires. Toutefois, à compter du début de 2014, seuls 30% des programmeurs Python étaient passés à la version 3.x. Par conséquent, pour les autres 70% des programmeurs Python utilisant encore la version 2.x, je vais donner une réponse. Au taux actuel, 3.x ne dépassera pas 2.x avant ~ 2019 et 2.x ne disparaîtra pas avant ~ 2027. Je pense donc que cette réponse sera utile pendant plusieurs années. 

Je veux aborder les problèmes énumérés ci-dessus un à la fois, je vais donc commencer par changer votre fonction, consistant à utiliser print comme sortie en utilisant return. Pas de surprises, voici ce code:

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

Il suffit donc de simuler raw_input. Assez facile - La réponse d'Omid Raha à cette question précise nous montre comment faire cela en analysant l'implémentation __builtins__.raw_input avec notre implémentation fictive. Sauf que sa réponse n'était pas correctement organisée en une TestCase et des fonctions, je vais donc le démontrer.

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

Petite remarque sur les conventions de dénomination Python - les variables requises par l'analyseur mais non utilisées sont généralement nommées _, comme dans le cas de la variable non utilisée de lambda (qui est normalement l'invite affichée à l'utilisateur dans le cas du raw_input, en cas vous vous demandez pourquoi cela est nécessaire dans ce cas).

Quoi qu'il en soit, c'est désordonné et redondant. Je vais donc supprimer la répétition en ajoutant une variable contextmanager, qui permettra de simples déclarations with.

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')

Je pense que cela répond bien à la première partie de ceci. Passons à la deuxième partie - en vérifiant print. J'ai trouvé cela beaucoup plus compliqué - j'aimerais entendre si quelqu'un a une meilleure réponse.

Quoi qu'il en soit, l'instruction print ne peut pas être remplacée, mais si vous utilisez plutôt les fonctions print() (comme vous le devriez) et from __future__ import print_function, vous pouvez utiliser les éléments suivants:

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')

La difficulté ici est que vous devez yield une réponse avant que le bloc with ne soit entré. Mais vous ne pouvez pas savoir quelle sera cette réponse tant que la fonction print() dans le bloc with n'aura pas été appelée. Ce serait bien si les chaînes étaient mutables, mais elles ne le sont pas. Donc, au lieu de cela, une petite classe de promesse ou de procuration a été faite - PromiseString. Cela ne fait que deux choses: permettre à une chaîne (ou autre chose, vraiment) d'être définie et nous indiquer si elle est égale à une chaîne différente. PromiseString est yielded et est ensuite défini sur la valeur qui serait normalement print dans le bloc with.

J'espère que vous apprécierez toute cette supercherie que j'ai écrite depuis qu'il m'a fallu environ 90 minutes pour la mettre ensemble ce soir. J'ai testé tout ce code et vérifié que tout fonctionnait avec Python 2.7.

27
ArtOfWarfare

Je viens de rencontrer le même problème, mais je viens de me moquer de __builtin__.raw_input.

Testé uniquement sur Python 2. pip install mock si le paquet n’est pas déjà installé.

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')

Vous pouvez également utiliser la bibliothèque genty pour simplifier les deux tests:

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')
6
Jeff-Meadows

J'utilise Python 3.4 et j'ai dû adapter les réponses ci-dessus. Ma solution prend en compte le code courant dans la méthode personnalisée runTest et vous montre comment appliquer des correctifs aux fonctions input() et print(). Voici le code qui s'exécute: importer unittest depuis io import StringIO depuis le patch d'importation unittest.mock

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 __== '__main__':
    unittest.main()
5
tbc0
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
0
Omid Raha