170 votes

Utilisation du patch.object fantaisie de python pour changer la valeur de retour d'une méthode appelée dans une autre méthode

Est-il possible de simuler la valeur de retour d'une fonction appelée dans une autre fonction que j'essaie de tester ? Je voudrais que la méthode simulée (qui sera appelée dans de nombreuses méthodes que je teste) renvoie les variables spécifiées à chaque fois qu'elle est appelée. Par exemple :

class Foo:
    def method_1():
       results = uses_some_other_method()
    def method_n():
       results = uses_some_other_method()

Dans le test unitaire, je voudrais utiliser mock pour changer la valeur de retour de uses_some_other_method() de sorte que chaque fois qu'il est appelé dans Foo il retournera ce que j'ai défini dans @patch.object(...)

240voto

Silfheed Points 2818

Il y a deux façons de le faire : avec patch et avec patch.object.

Patch suppose que vous n'importez pas directement l'objet mais qu'il est utilisé par l'objet que vous testez comme dans le cas suivant

#foo.py
def some_fn():
    return 'some_fn'

class Foo(object):
    def method_1(self):
        return some_fn()

#bar.py
import foo
class Bar(object):
    def method_2(self):
        tmp = foo.Foo()
        return tmp.method_1()

#test_case_1.py
import bar
from mock import patch

@patch('foo.some_fn')
def test_bar(mock_some_fn):
    mock_some_fn.return_value = 'test-val-1'
    tmp = bar.Bar()
    assert tmp.method_2() == 'test-val-1'
    mock_some_fn.return_value = 'test-val-2'
    assert tmp.method_2() == 'test-val-2'

Si vous importez directement le module à tester, vous pouvez utiliser patch.object comme suit :

#test_case_2.py
import foo
from mock import patch

@patch.object(foo, 'some_fn')
def test_foo(test_some_fn):
    test_some_fn.return_value = 'test-val-1'
    tmp = foo.Foo()
    assert tmp.method_1() == 'test-val-1'
    test_some_fn.return_value = 'test-val-2'
    assert tmp.method_1() == 'test-val-2'

Dans les deux cas, some_fn sera 'un-mocked' après que la fonction de test soit terminée.

Éditer : Afin de simuler plusieurs fonctions, il suffit d'ajouter des décorateurs à la fonction et d'ajouter des arguments pour prendre les paramètres supplémentaires.

@patch.object(foo, 'some_fn')
@patch.object(foo, 'other_fn')
def test_foo(test_other_fn, test_some_fn):
    ...

Notez que plus le décorateur est proche de la définition de la fonction, plus il se trouve tôt dans la liste des paramètres.

55voto

chipz Points 594

Cela peut être fait avec quelque chose comme ceci :

# foo.py
class Foo:
    def method_1():
        results = uses_some_other_method()

# testing.py
from mock import patch

@patch('Foo.uses_some_other_method', return_value="specific_value"):
def test_some_other_method(mock_some_other_method):
    foo = Foo()
    the_value = foo.method_1()
    assert the_value == "specific_value"

Voici une source que vous pouvez lire : Parcheando au mauvais endroit.

16voto

JackWu Points 330

Laissez-moi clarifier ce dont vous parlez : vous voulez tester Foo dans un testcase, qui appelle la méthode externe uses_some_other_method . Au lieu d'appeler la méthode réelle, vous voulez simuler la valeur de retour.

class Foo:
    def method_1():
       results = uses_some_other_method()
    def method_n():
       results = uses_some_other_method()

Supposons que le code ci-dessus soit dans foo.py et uses_some_other_method est défini dans le module bar.py . Voici l'unittest :

import unittest
import mock

from foo import Foo

class TestFoo(unittest.TestCase):

    def setup(self):
        self.foo = Foo()

    @mock.patch('foo.uses_some_other_method')
    def test_method_1(self, mock_method):
        mock_method.return_value = 3
        self.foo.method_1(*args, **kwargs)

        mock_method.assert_called_with(*args, **kwargs)

Si vous voulez changer la valeur de retour chaque fois que vous avez passé des arguments différents, mock fournit side_effect .

4voto

UncleSaam Points 156

Pour compléter la réponse de Silfheed, qui était utile, j'ai dû patcher plusieurs méthodes de l'objet en question. J'ai trouvé plus élégant de le faire de cette façon :

Étant donné la fonction suivante à tester, située dans module.a_function.to_test.py :

from some_other.module import SomeOtherClass

def add_results():
    my_object = SomeOtherClass('some_contextual_parameters')
    result_a = my_object.method_a()
    result_b = my_object.method_b()

    return result_a + result_b

Pour tester cette fonction (ou méthode de classe, peu importe), on peut patcher plusieurs méthodes de la classe SomeOtherClass en utilisant patch.object() en combinaison avec sys.modules :

@patch.object(sys.modules['module.a_function.to_test'], 'SomeOtherClass')
def test__should_add_results(self, mocked_other_class):
  mocked_other_class().method_a.return_value = 4
  mocked_other_class().method_b.return_value = 7

  self.assertEqual(add_results(), 11)

Cela fonctionne quel que soit le nombre de méthodes de SomeOtherClass que vous devez patcher, avec des résultats indépendants.

De plus, en utilisant la même méthode Parcheando, une instance réelle de SomeOtherClass peut être retourné si nécessaire :

@patch.object(sys.modules['module.a_function.to_test'], 'SomeOtherClass')
def test__should_add_results(self, mocked_other_class):
  other_class_instance = SomeOtherClass('some_controlled_parameters')
  mocked_other_class.return_value = other_class_instance 
  ...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X