alvas
alvas

Reputation: 122330

How to check of exceptions in nested functions when using MagicMock objects in Pytest?

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

Answers (1)

hoefling
hoefling

Reputation: 66591

There are two issues with the test in question.

  1. 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
    
  2. 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

Related Questions