298

We have been using Mock for python for a while.

Now, we have a situation in which we want to mock a function

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

Normally, the way to mock this would be (assuming foo being part of an object)

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

Even, if i call foo() a couple of times i can use

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

Now, I am facing a situation in which I want to return a fixed value when the input parameter has a particular value. So if let's say "my_param" is equal to "something" then I want to return "my_cool_mock"

This seems to be available on mockito for python

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

I have been searching on how to achieve the same with Mock with no success?

Any ideas?

4
  • 3
    May be this answer will help - stackoverflow.com/a/7665754/234606
    – naiquevin
    Commented Apr 23, 2013 at 6:05
  • @naiquevin This perfectly solves the problem mate, thanks! Commented Apr 23, 2013 at 6:18
  • 1
    If your project uses pytest, for such a purpose you may want to leverage monkeypatch. Monkeypatch is more for "replace this function for sake of testing," whereas Mock is what you use when you also want to check the mock_calls or make assertions about what it was called with and so on. There is a place for both, and I often use both at different times in a given test file.
    – floer32
    Commented Oct 8, 2018 at 22:51
  • Possible duplicate of Python Mock object with method called multiple times
    – Melebius
    Commented Jun 12, 2019 at 12:59

9 Answers 9

344

If side_effect_func is a function then whatever that function returns is what calls to the mock return. The side_effect_func function is called with the same arguments as the mock. This allows you to vary the return value of the call dynamically, based on the input:

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

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

4
  • 43
    Just to make the answer easier, could you rename the side_effect function to something else? (i know, i know, it is pretty simple, but improves readability the fact that function name and param name are different :) Commented Apr 23, 2013 at 6:21
  • 12
    @JuanAntonioGomezMoriano I could, but in this case I'm just directly quoting the documentation, so I'm a bit loathe to edit the quote if it's not specifically broken.
    – Amber
    Commented Apr 23, 2013 at 14:14
  • and just to be pedantic all these years later, but side effect is the accurate term: en.wikipedia.org/wiki/Side_effect_(computer_science)
    – lsh
    Commented Jul 4, 2017 at 2:11
  • 14
    @Ish they're not complaining about the name of CallableMixin.side_effect, but that the separate function defined in the example has the same name.
    – OrangeDog
    Commented May 8, 2018 at 14:54
116

As indicated at Python Mock object with method called multiple times

A solution is to write my own 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

That does the trick

1
  • 11
    This made it clearer for me than the selected answer, so thank you for answering your own question :) Commented Aug 10, 2018 at 19:59
32

Side effect takes a function (which can also be a lambda function), so for simple cases you may use:

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

If you "want to return a fixed value when the input parameter has a particular value", maybe you don't even need a mock and could use a dict along with its get method:

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

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

This works well when your fake's output is a mapping of input. When it's a function of input I'd suggest using side_effect as per Amber's answer.

You can also use a combination of both if you want to preserve Mock's capabilities (assert_called_once, call_count etc):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
2
  • 3
    This is very clever.
    – emyller
    Commented Jun 30, 2020 at 23:11
  • 3
    How would this look with multiple parameters? I tried it with tuples, as dict keys, but that didn't end well. Commented Nov 15, 2023 at 14:25
10

I've ended up here looking for "how to mock a function based on input arguments" and I finally solved this creating a simple aux function:

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

Now:

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'

Hope this will help someone!

0
10

Although side_effect can achieve the goal, it is not so convenient to setup side_effect function for each test case.

I write a lightweight Mock (which is called NextMock) to enhance the built-in mock to address this problem, here is a simple example:

from nextmock import Mock

m = Mock()

m.with_args(1, 2, 3).returns(123)

assert m(1, 2, 3) == 123
assert m(3, 2, 1) != 123

It also supports argument matcher:

from nextmock import Arg, Mock

m = Mock()

m.with_args(1, 2, Arg.Any).returns(123)

assert m(1, 2, 1) == 123
assert m(1, 2, "123") == 123

Hope this package could make testing more pleasant. Feel free to give any feedback.

5

You can also use partial from functools if you want to use a function that takes parameters but the function you are mocking does not. E.g. like this:

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

This will return a callable that doesn't accept parameters (like Django's timezone.now()), but my mock_year function does.

1
  • Thanks for this elegant solution. I like to add that if your original function has additional parameters, they need to be called with keywords in you production code or this approach won't work. You get the error: got multiple values for argument. Commented Mar 6, 2020 at 10:43
2

Just to show another way of doing it:

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
2

You can also use @mock.patch.object:

Let's say a module my_module.py uses pandas to read from a database and we would like to test this module by mocking pd.read_sql_table method (which takes table_name as argument).

What you can do is to create (inside your test) a db_mock method that returns different objects depending on the argument provided:

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

In your test function you then do:

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`

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.