DEV Community

Kenichiro Nakamura
Kenichiro Nakamura

Posted on

python: use multiple patch decorators to mock functions

I wrote how to mock in the previous article. This time, I mock multiple functions in the test to see how I can handle them.

Structures and code

This is almost same as before, just adding one more function in util.py.

src/
    ├── my.py  
    ├── my_modules/
    │   ├── __init__.py
    │   └── util.py
    └── tests/
        ├── __init__.py
        ├── test_my.py
        └── test_unit.py
Enter fullscreen mode Exit fullscreen mode

my.py

from my_modules.util import util, get_data

def main():
    data = get_data()
    return util(data)

if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

util.py

from datetime import datetime

def util(input: str) -> str:
    input = add_time(input)
    return f"util: {input}"

def add_time(input: str) -> str:
    return f"{datetime.now()}: {input}"

def get_data() -> str:
    return "Return some data"
Enter fullscreen mode Exit fullscreen mode

Add a unit test for main function

To test the main method in the my.py, I need to mock both util and get_data method. Let's do it.

Firstly, I added another patch and specify return_value. It doesn't change the test body though.

@patch("my.util", Mock(return_value="dummy"))
@patch("my.get_data", Mock(return_value="some data"))
def test_main():
    result = main()
    assert result =='dummy'
Enter fullscreen mode Exit fullscreen mode

Let's receive the mock objects as the arguments.

@patch("my.util")
@patch("my.get_data")
def test_main_util_called_with_expected_parameter(get_data_mock, util_mock):
    get_data_mock.return_value = 'some data'
    util_mock.return_value = 'dummy'
    result = main()
    assert result =='dummy'
    util_mock.assert_any_call('some data')
Enter fullscreen mode Exit fullscreen mode

The interesting part is the order of argument. As you see, I can get the mock from bottom up order of the patch decorators. I firstly though I can receive the mocks in the same order as the decorators, but I was wrong.

Finally, let's try with statement.

def test_main_util_called_with_expected_parameter_with():
    with patch("my.util") as util_mock:
        util_mock.return_value = 'dummy'
        with patch("my.get_data") as get_data_mock:
            get_data_mock.return_value = 'some data'
            result = main()
    assert result =='dummy'
    util_mock.assert_any_call('some data')
Enter fullscreen mode Exit fullscreen mode

I am not 100% sure if this is the correct way to implement, but it works as expected anyway.

I just added one more example. This time, I specify the Mock object in the first decorator only. In this case, I can receive just one mock object as an argument.

@patch("my.util", Mock(return_value="dummy"))
@patch("my.get_data")
def test_main_get_data_called(get_data_mock):
    get_data_mock.return_value = 'some data'
    result = main()
    assert result =='dummy'
    assert get_data_mock.called
Enter fullscreen mode Exit fullscreen mode

I check if get_data function is called.

Summary

I understand when I get the mock object as the arguments. It's a bit confusing but once I understand, it's very useful.

Top comments (0)