DEV Community

Kenichiro Nakamura
Kenichiro Nakamura

Posted on

python: unit test with mock functions from different modules

I recently started learning python 3 and unit test with pytest and unittest.

As I struggle to figure out how to mock in several situations, I am taking note here so that anyone has same issue maybe find this useful someday.

Structures and code

Before writing tests, this is my folder and files structures.

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

def main():
    return util('my input')

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}"
Enter fullscreen mode Exit fullscreen mode

Add a unit test for util function

For util method, I need to mock add_time that is from the same file. I found several ways to achieve this, and this is one of them.

test_unit.py

from unittest.mock import patch, Mock
from my_modules.util import util

@patch('my_modules.util.add_time', Mock(return_value='dummy'))
def test_util():
    expected = 'util: dummy'
    input = 'input'
    input = util(input)
    assert expected == input
Enter fullscreen mode Exit fullscreen mode

I used unittest.mock.patch function as a decorator and specify the desired return value as part of the Mock object. The namespace for add_time is the same as the util function as they are in the same module.

Add a unit test for add_time function

To unit test add_time function, I need to mock datetime.now() function. I again use the unittest.mock.patch. This time, I need to create the Mock with a bit more code as I need to mock a function, rather than the simple return_value nor an attribute.

from datetime import datetime
from unittest.mock import patch, Mock
from my_modules.util import add_time

@patch('my_modules.util.datetime', Mock(**{"now.return_value": datetime(2023, 1, 1)}))
def test_add_time():
    expected = f'{datetime(2023, 1, 1)}: input'
    input = 'input'
    input = add_time(input)
    assert expected == input
Enter fullscreen mode Exit fullscreen mode

I can pass a dictionary that contains attributes and methods information to the Mock object. As I mock now function, I use "now.return_value":<some date>.

If it's an attribute, I can simply pass it to the Mock like Mock(attribute_name=<value>) or as part of the dictionary like {"attribute":<value>}

The module name is a bit interesting as I need to specify my_modules.util.datetime. The reason is that as soon as the datetime is imported to the util.py, it becomes part of the same module, that was quite confusing to me.

I can show another sample of this in the next test.

Add a unit test for main function

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

test_my.py

from my import main
from unittest.mock import patch, Mock

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

Even though the util function comes from the my_modules module, in the test time, it becomes my.util namespace as I previously explained.

I specify the Mock object in the decorator, but I cannot if the util function is actually called with the expected parameters. So, let's accept the mock inside the test function.

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

This time, I use the decorator without passing the Mock object. Instead, I received it in the argument as util_mock, then I specify the return_value.

As I have access to the mock, I can then assert it the method is called with the expected arguments.

Lastly, I also learnt that I can use with statement as well to achieve the same.

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

By doing this, I don't need the decorator.

All the tests run successfully!

test results

Summary

I actually don't know yet which is the best way to mock functions. I believe each one has pros and cons, so if any of you have any suggestions or opinions, please let me know in the comment! Thanks.

Top comments (1)

Collapse
 
davidfune profile image
DavidFune

I followed the same folder structure as the example, but I receive the error ModuleNotFoundError: No module named 'my' when running pytest in the root of the project.