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?

Juan Antonio Gomez Moriano
la source
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
Copie

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

ambre
la source
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

Juan Antonio Gomez Moriano
la source
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))
Shubham Chaudhary
la source
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!

Manu
la source
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.

Hernan
la source
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
Caleb
la source
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`
Tomasz Bartkowiak
la source
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
Arseniy Panfilov
la source
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
Novice
la source