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
my.py
from my_modules.util import util
def main():
return util('my input')
if __name__ == '__main__':
main()
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}"
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
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
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'
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')
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')
By doing this, I don't need the decorator.
All the tests run successfully!
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)
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.