web-dev-qa-db-fra.com

Python renvoie un objet MagicMock au lieu de return_value

J'ai un fichier python file a.py qui contient deux classes A et B.

class A(object):
    def method_a(self):
        return "Class A method a"

class B(object):
    def method_b(self):
        a = A()
        print a.method_a()

Je voudrais unittest method_b dans la classe B en se moquant A. Voici le contenu du fichier testa.py dans ce but:

import unittest
import mock
import a


class TestB(unittest.TestCase):

    @mock.patch('a.A')
    def test_method_b(self, mock_a):
        mock_a.method_a.return_value = 'Mocked A'
        b = a.B()
        b.method_b()


if __== '__main__':
    unittest.main()

Je m'attends à avoir Mocked A dans la sortie. Mais ce que je reçois c'est:

<MagicMock name='A().method_a()' id='4326621392'>

Où est-ce que je me trompe?

49

Lorsque vous @mock.patch('a.A'), vous remplacez la classe A dans le code testé par mock_a.

Dans B.method_b, Vous définissez ensuite a = A(), qui est maintenant a = mock_a() - c'est-à-dire a est le return_value De mock_a. Comme vous n'avez pas spécifié cette valeur, il s'agit d'un MagicMock normal; ce n'est pas configuré non plus, donc vous obtenez la réponse par défaut (encore un autre MagicMock) lorsque vous appelez des méthodes dessus.

Au lieu de cela, vous voulez configurer le return_value De mock_a pour avoir la méthode appropriée, que vous pouvez faire soit:

mock_a().method_a.return_value = 'Mocked A' 
    # ^ note parentheses

ou peut-être plus explicitement:

mock_a.return_value.method_a.return_value = 'Mocked A'

Votre code aurait fonctionné dans le cas a = A (Assigner la classe, pas créer une instance), car alors a.method_a() aurait déclenché votre méthode fictive.

54
jonrsharpe

Je préfère pytest avec mocker fixture . Voici le même test, en utilisant pytest et mocker:

import a


class TestB:
    def test_method_b(self, mocker):
        mock_A = mocker.MagicMock(name='A', spec=a.A)
        mocker.patch('a.A', new=mock_A)
        mock_A.return_value.method_a.return_value = 'Mocked A'

        b = a.B()
        b.method_b()

Vous trouverez peut-être que la façon dont j'ai écrit le test est plus intéressante que le test lui-même - j'ai créé une bibliothèque python pour m'aider à utiliser la syntaxe.

Voici comment j'ai abordé votre problème de manière systématique:

Nous commençons par le test que vous voulez et ma bibliothèque d'assistance:

import a

from mock_autogen.pytest_mocker import PytestMocker


class TestB:
    def test_method_b(self, mocker):
        # this would output the mocks we need
        print(PytestMocker(a).mock_classes().prepare_asserts_calls().generate())

        # your original test, without the mocks
        b = a.B()
        b.method_b()

Maintenant, le test ne fait pas grand chose, mais la sortie imprimée est utile:

# mocked classes
mock_A = mocker.MagicMock(name='A', spec=a.A)
mocker.patch('a.A', new=mock_A)
mock_B = mocker.MagicMock(name='B', spec=a.B)
mocker.patch('a.B', new=mock_B)
# calls to generate_asserts, put this after the 'act'
import mock_autogen
print(mock_autogen.generator.generate_asserts(mock_A, name='mock_A'))
print(mock_autogen.generator.generate_asserts(mock_B, name='mock_B'))

Maintenant, je mets une seule maquette pour A avant l'appel à B() et la section generate_asserts Après, comme cela (pas besoin de la précédente impression, donc je enlevé):

def test_method_b(self, mocker):
    # mocked classes
    mock_A = mocker.MagicMock(name='A', spec=a.A)
    mocker.patch('a.A', new=mock_A)

    # your original test, without the mocks
    b = a.B()
    b.method_b()

    # calls to generate_asserts, put this after the 'act'
    import mock_autogen
    print(mock_autogen.generator.generate_asserts(mock_A, name='mock_A'))

Après cette exécution de test, nous avons obtenu de précieuses contributions:

assert 1 == mock_A.call_count
mock_A.assert_called_once_with()
mock_A.return_value.method_a.assert_called_once_with()
mock_A.return_value.method_a.return_value.__str__.assert_called_once_with()

Les deux premières lignes vérifient que le A mock a été initialisé une fois, sans paramètre. La troisième ligne vérifie que method_a A été appelé, alors que la 4ème ligne peut être la plus utile pour vous et vous aurait peut-être permis de gagner beaucoup de temps.

mock_A.return_value.method_a.return_value.__str__.assert_called_once_with()

Vous voyez que la valeur renvoyée de method_a A été appliquée avec str (en raison de la fonction print.). le remplacer par votre chaîne désirée est assez facile:

mock_A.return_value.method_a.return_value = 'Mocked A'

Et c’est comme ça que j’ai eu la méthode de test complète mentionnée ci-dessus.

0
Peter K