0

I have some method in a Python module db_access called read_all_rows that takes 2 strings as parameters and returns a list:

def read_all_rows(table_name='', mode=''):
    return [] # just an example

I can mock this method non-conditionally using pytest like:

mocker.patch('db_access.read_all_rows', return_value=['testRow1', 'testRow2'])

But I want to mock its return value in pytest depending on table_name and mode parameters, so that it would return different values of parameters and combinations of them. And to make this as simple as possible.

The pseudocode of what I want:

when(db_access.read_all_rows).called_with('table_name1', any_string()).then_return(['testRow1'])
when(db_access.read_all_rows).called_with('table_name2' 'mode1').then_return(['testRow2', 'tableRow3'])
when(db_access.read_all_rows).called_with('table_name2' 'mode2').then_return(['testRow2', 'tableRow3'])

You can see that the 1st call is mocked for "any_string" placeholder.

I know that it can be achieved with side_effect like

def mock_read_all_rows:
    ...

mocker.patch('db_access.read_all_rows', side_effect=mock_read_all_rows)

but it is not very convenient because you need to add extra function which makes the code cumbersome. Even with lambda it is not so convenient because you would need to handle all conditions manually.

How this could be solved in a more short and readable way (ideally in a single line of code for each mock condition)?

P.S. In Java Mockito it is can be easily acheived with single line of code for each condition like

when(dbAccess.readAllRows(eq("tableName1"), any())).thenReturn(List.of(value1, value2));
...

but can I do this with Python's pytest mocker.patch?

dimnnv
  • 678
  • 3
  • 8
  • 21
  • Does this answer your question? [Mocking python function based on input arguments](https://stackoverflow.com/questions/16162015/mocking-python-function-based-on-input-arguments) – Cpt.Hook Dec 14 '22 at 08:01
  • Nope, as I said earlier, the side_effect is too cumbersome compared to the expected result (like in Java Mockito) and I'm looking for more compact and neat solution. – dimnnv Dec 14 '22 at 08:04
  • Well, that is the pythonic way. You could create your own conditional mocking DSL upon this... – Cpt.Hook Dec 14 '22 at 08:07
  • can you please explain what do you mean by creating mocking DSL? Am I right that it is also not a single line of code like in Mockito? – dimnnv Dec 14 '22 at 08:09

1 Answers1

0

There is no "of the shelf one liner" as you know it from the mockito-library. The pythonic way how to mock based on input arguments is described in this answer.

However, nothing can stop you from creating your own single line wrapper and therefore have your own mocking interface (or domain specfic language (DSL)).


This simple wrapper should give you an idea. Place this class in a test helper module.

# tests/helper/mocking.py

class ConditionalMock:

    def __init__(self, mocker, path):
        self.mock = mocker.patch(path, new=self._replacement)
        self._side_effects = {}
        self._default = None
        self._raise_if_not_matched = True

    def expect(self, condition, return_value):
        condition = tuple(condition)
        self._side_effects[condition] = return_value
        return self

    def default_return(self, default):
        self._raise_if_not_matched = False
        self._default = default
        return self

    def _replacement(self, *args):
        if args in self._side_effects:
            return self._side_effects[args]
        if self._raise_if_not_matched:
            raise AssertionError(f'Arguments {args} not expected')
        return self._default

Now import it in the tests just as usual and use the new one-line conditional mocking interace

# tests/mocking_test.py
import time
from .helper.mocking import ConditionalMock

def test_conditional_mocker(mocker) -> None:
    ConditionalMock(mocker, 'time.strftime').expect('a', return_value='b').expect((1, 2), return_value='c')
    assert time.strftime('a') == 'b'
    assert time.strftime(1, 2) == 'c'

And result:

$ pytest tests/mocking_test.py
================================================================================================================================================= test session starts ==================================================================================================================================================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
plugins: mock-3.6.1
collected 1 item                                                                                                                                                                                                                                                                                                       

tests/mocking_test.py .
Cpt.Hook
  • 590
  • 13
  • 1
    Thanks for your effort. As I found out there is also already a library called [mockito-python](https://github.com/kaste/mockito-python) which does exactly what I expect, but unfortunately it is not plain Python / Pytest. – dimnnv Dec 15 '22 at 10:21
  • @dimnnv mockito-python is just portable Python code. Optional there is a plugin for it for pytest. What does "plain" mean? – herr.kaste Feb 08 '23 at 12:29
  • @herr.kaste I appreciate your efforts in developing this instrument. However, mockito-python is (at least the moment) is a relatively small library which one usually needs to include in the project as a dependency. Pytest is a defacto standard for writing tests and most usually one already has it in project requirements. Strictly speaking, pytest is not a part of Python so it cannot be considered "plain python", but it is a widely-used instrument. I tend to keep less dependencies in my projects whenever possible so that's I am looking for pytest-based solution. – dimnnv Feb 08 '23 at 18:50