2

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?

alvas
  • 115,346
  • 109
  • 446
  • 738
  • In your example code, you don't mock anything, you just create mocks without connecting them to real objects - probably forgot to add some code. Can you show us an actual reproducible example? – MrBean Bremen Apr 16 '21 at 15:23
  • I think that's my question too, I suspect it's not mocking anything but what I want to mock is the returns of the `func_z` according to the `SIDE_EFFECTS` – alvas Apr 16 '21 at 16:57
  • 1
    `assert outer_func(a, b, mock_creds) == expected`? – hoefling Apr 16 '21 at 22:55

1 Answers1

2

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.
hoefling
  • 59,418
  • 12
  • 147
  • 194
  • 1
    Good answer as usual, hoefling! Humbly mentioning, in the final example, my plugin [pytest-raisin](https://github.com/wimglenn/pytest-raisin) allows to use an instance to assert the exc type + message together `with pytest.raises(XError("An XError message if passed")):` – wim Apr 22 '21 at 16:24
  • 1
    Thanks @wim! This is indeed an interesting addition to `pytest`; I think I can benefit from it myself, as I need to assert a lot of `pydantic.ValidationError`s in one project at work... – hoefling Apr 22 '21 at 16:52