Fonction python moqueuse basée sur des arguments d'entrée


150

Nous utilisons Mock pour python depuis un certain temps.

Maintenant, nous avons une situation dans laquelle nous voulons nous moquer d'une fonction

def foo(self, my_param):
    #do something here, assign something to my_result
    return my_result

Normalement, la façon de se moquer de cela serait (en supposant que foo fasse partie d'un objet)

self.foo = MagicMock(return_value="mocked!")

Même si j'appelle foo () plusieurs fois, je peux utiliser

self.foo = MagicMock(side_effect=["mocked once", "mocked twice!"])

Maintenant, je suis confronté à une situation dans laquelle je souhaite renvoyer une valeur fixe lorsque le paramètre d'entrée a une valeur particulière. Donc si disons que "mon_param" est égal à "quelque chose" alors je veux retourner "mon_cool_mock"

Cela semble être disponible sur mockito pour python

when(dummy).foo("something").thenReturn("my_cool_mock")

J'ai cherché comment réaliser la même chose avec Mock sans succès?

Des idées?


2
Peut-être que cette réponse vous aidera - stackoverflow.com/a/7665754/234606
naiquevin

@naiquevin Cela résout parfaitement le problème, merci!
Juan Antonio Gomez Moriano

Je ne savais pas que vous pouviez utiliser Mocktio avec Python, +1 pour ça!
Ben

Si votre projet utilise pytest, dans ce but, vous voudrez peut-être tirer parti monkeypatch. Monkeypatch est plus destiné à "remplacer cette fonction pour des raisons de test", alors que Mock est ce que vous utilisez lorsque vous souhaitez également vérifier mock_callsou faire des affirmations sur ce avec quoi il a été appelé, etc. Il y a une place pour les deux, et j'utilise souvent les deux à des moments différents dans un fichier de test donné.
driftcatcher

Réponses:


187

Si side_effectest une fonction, tout ce que cette fonction renvoie est ce qui appelle le retour simulé. La side_effectfonction est appelée avec les mêmes arguments que le mock. Cela vous permet de faire varier la valeur de retour de l'appel de manière dynamique, en fonction de l'entrée:

>>> def side_effect(value):
...     return value + 1
...
>>> m = MagicMock(side_effect=side_effect)
>>> m(1)
2
>>> m(2)
3
>>> m.mock_calls
[call(1), call(2)]

http://www.voidspace.org.uk/python/mock/mock.html#calling


25
Pour faciliter la réponse, pourriez-vous renommer la fonction side_effect en autre chose? (Je sais, je sais, c'est assez simple, mais améliore la lisibilité du fait que le nom de la fonction et le nom du paramètre sont différents :)
Juan Antonio Gomez Moriano

6
@JuanAntonioGomezMoriano Je pourrais, mais dans ce cas, je cite directement la documentation, donc je déteste un peu modifier la citation si elle n'est pas spécifiquement cassée.
Ambre

et juste pour être pédant toutes ces années plus tard, mais side effectc'est le terme exact: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh

7
@Ish ils ne se plaignent pas du nom de CallableMixin.side_effect, mais que la fonction distincte définie dans l'exemple a le même nom.
OrangeDog

48

Comme indiqué à l' objet Python Mock avec la méthode appelée plusieurs fois

Une solution est d'écrire mon propre side_effect

def my_side_effect(*args, **kwargs):
    if args[0] == 42:
        return "Called with 42"
    elif args[0] == 43:
        return "Called with 43"
    elif kwargs['foo'] == 7:
        return "Foo is seven"

mockobj.mockmethod.side_effect = my_side_effect

Ça fait l'affaire


2
Cela a été plus clair pour moi que la réponse sélectionnée, alors merci d'avoir répondu à votre propre question :)
Luca Bezerra

15

L'effet secondaire prend une fonction (qui peut également être une fonction lambda ), donc pour les cas simples, vous pouvez utiliser:

m = MagicMock(side_effect=(lambda x: x+1))

4

J'ai fini par chercher ici " comment simuler une fonction basée sur des arguments d'entrée " et j'ai finalement résolu ce problème en créant une simple fonction aux:

def mock_responses(responses, default_response=None):
  return lambda input: responses[input] if input in responses else default_response

Maintenant:

my_mock.foo.side_effect = mock_responses(
  {
    'x': 42, 
    'y': [1,2,3]
  })
my_mock.goo.side_effect = mock_responses(
  {
    'hello': 'world'
  }, 
  default_response='hi')
...

my_mock.foo('x') # => 42
my_mock.foo('y') # => [1,2,3]
my_mock.foo('unknown') # => None

my_mock.goo('hello') # => 'world'
my_mock.goo('ey') # => 'hi'

J'espère que cela aidera quelqu'un!


2

Vous pouvez également utiliser partialfrom functoolssi vous souhaitez utiliser une fonction qui prend des paramètres mais que la fonction dont vous vous moquez ne le fait pas. Par exemple, comme ceci:

def mock_year(year):
    return datetime.datetime(year, 11, 28, tzinfo=timezone.utc)
@patch('django.utils.timezone.now', side_effect=partial(mock_year, year=2020))

Cela renverra un appelable qui n'accepte pas les paramètres (comme timezone.now () de Django), mais ma fonction mock_year le fait.


Merci pour cette solution élégante. J'aime ajouter que si votre fonction d'origine a des paramètres supplémentaires, ils doivent être appelés avec des mots-clés dans votre code de production, sinon cette approche ne fonctionnera pas. Vous obtenez l'erreur: got multiple values for argument.
Erik Kalkoken

1

Juste pour montrer une autre façon de le faire:

def mock_isdir(path):
    return path in ['/var/log', '/var/log/apache2', '/var/log/tomcat']

with mock.patch('os.path.isdir') as os_path_isdir:
    os_path_isdir.side_effect = mock_isdir

1

Vous pouvez également utiliser @mock.patch.object:

Disons qu'un module my_module.pyutilise pandaspour lire à partir d'une base de données et que nous aimerions tester ce module par une pd.read_sql_tableméthode moqueuse (qui prend table_namecomme argument).

Ce que vous pouvez faire est de créer (à l'intérieur de votre test) une db_mockméthode qui renvoie différents objets en fonction de l'argument fourni:

def db_mock(**kwargs):
    if kwargs['table_name'] == 'table_1':
        # return some DataFrame
    elif kwargs['table_name'] == 'table_2':
        # return some other DataFrame

Dans votre fonction de test, vous faites ensuite:

import my_module as my_module_imported

@mock.patch.object(my_module_imported.pd, "read_sql_table", new_callable=lambda: db_mock)
def test_my_module(mock_read_sql_table):
    # You can now test any methods from `my_module`, e.g. `foo` and any call this 
    # method does to `read_sql_table` will be mocked by `db_mock`, e.g.
    ret = my_module_imported.foo(table_name='table_1')
    # `ret` is some DataFrame returned by `db_mock`

1

Si vous «souhaitez renvoyer une valeur fixe lorsque le paramètre d'entrée a une valeur particulière» , peut-être n'avez-vous même pas besoin d'un simulacre et pourriez utiliser a dictavec sa getméthode:

foo = {'input1': 'value1', 'input2': 'value2'}.get

foo('input1')  # value1
foo('input2')  # value2

Cela fonctionne bien lorsque la sortie de votre faux est un mappage d'entrée. Quand c'est une fonction d'entrée, je suggère d'utiliser side_effectselon la réponse d' Amber .

Vous pouvez également utiliser une combinaison des deux si vous souhaitez conserver Mockles capacités de ( assert_called_once, call_countetc):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get

C'est très intelligent.
emyller

-6

Je sais que c'est une question assez ancienne, pourrait aider comme amélioration en utilisant python lamdba

self.some_service.foo.side_effect = lambda *args:"Called with 42" \
            if args[0] == 42 \
            else "Called with 42" if args[0] == 43 \
            else "Called with 43" if args[0] == 43 \
            else "Called with 45" if args[0] == 45 \
            else "Called with 49" if args[0] == 49 else None
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.