Reputation: 122330
I've a function (myfunc
), with a validation input a
and and c
credentials that will setup a service to call func_z
.
For some validation input, func_z
will throw an error, and in other cases, it'll return some dictionary of values. And I've an outer_func
that modifies the output of func_z
.
I've tried to mock the func_z
as such:
class XError(Excception):
pass
def myfunc(a, b, c):
x = c.setup_client('x')
try:
x.func_z(a) # Check if x.func_z(a) works proper
except:
raise XError
return b**2
def outer_func(a, b, c):
return myfunc(a, b, c) + 1
When testing the function, I had to mock the c
credentials. So I tried to test with:
import pytest
from unittest.mock import MagicMock
test_data = ( (('fr', 5), 26), ('de', 7, 50), (('zh', 5), XError) )
SIDE_EFFECTS = {'fr': {'status': 'okay'}, 'de': {'status': 'okay'}, 'zh': XError}
@pytest.mark.parametrize("a, b", test_data)
def mytest(a, b, expected):
mock_creds = MagicMock()
mock_service = MagicMock()
mock_creds.setup_client.return_value = mock_service
mock_service.func_z.return_value = SIDE_EFFECTS[a]
assert outer_func(a, b, mock_creds) == expected
Somehow the XError
didn't get raised in the pytest and the output returns 26
for ('zh', 5)
inputs instead of XError.
But it seems like I'm not mocking anything.
Did I use the return value in the mock objects wrongly?
Is it possible to allow and check for raised error or outputs with mock objects in pytest?
Upvotes: 2
Views: 1223
Reputation: 66591
There are two issues with the test in question.
Returning an exception in mock:
mock_service.func_z.return_value = XError
is effectively a
def func_z():
return XError
Surely this is not what you want. Instead, you want func_z
mock to raise an error; for that, you need to use side_effect
:
mock_service.func_z.side_effect = XError
Asserting an exception is returned from tested function: the outer_func
doesn't return an exception, but raises it, so
assert outer_func(a, b, mock_creds) == expected
will fail for (('zh', 5), XError)
parameter as outer_func
will not return.
Instead, write two separate tests since you have two different paths the code can take in myfunc
; the test_happy
covers the path with no exception and the path with error raising is covered by test_xerror
. The code common to both tests (assembling the mock_service
) is moved out to a fixture.
@pytest.fixture
def mock_creds():
mock_creds = MagicMock()
mock_service = MagicMock()
mock_creds.setup_client.return_value = mock_service
return mock_creds
happy_data = (('fr', 5, 26), ('de', 7, 50))
SIDE_EFFECTS = {'fr': {'status': 'okay'}, 'de': {'status': 'okay'}}
@pytest.mark.parametrize("a, b, expected", happy_data)
def test_happy(mock_creds, a, b, expected):
func_z_return = SIDE_EFFECTS[a]
mock_creds.setup_client.return_value.func_z.return_value = func_z_return
assert outer_func(a, b, mock_creds) == expected
xerror_data = (('zh', 5), )
@pytest.mark.parametrize("a, b", xerror_data)
def test_xerror(mock_creds, a, b):
mock_creds.setup_client.return_value.func_z.side_effect = XError
with pytest.raises(XError):
outer_func(a, b, mock_creds)
Notice how pytest.raises
context is used for testing whether XError
is raised in the last test. If you want to test exception details, you can store the raised exception and inspect it after the with
block:
with pytest.raises(XError) as excinfo:
outer_func(a, b, mock_creds)
ex = excinfo.value
assert isinstance(ex, XError) # well, duh
assert ex.message == "An XError message if passed" # etc.
Upvotes: 2