0

I'm doing unit tests (using pytest/unittest/mockito, basically) and I need to mock the instantiation of a class implemented using Pydantic (BaseModel). Apparently it's not possible to mock a class in these circumstances without passing effectively valid data. I can't use "ANY()", because errors occur. Is there any way to mock this class without having to use valid data as arguments?

NOTE: Apparently the problem occurs because Pydantic is being used.

I've been doing a lot of research on the Internet, but no luck ... Any ideas?

Below are the codes I'm using in my tests in a very simplified way...

pydantic_class.py - Pydantic (BaseModel) Class

from pydantic import BaseModel
from some.path.sometypea import SomeTypeA
from some.path.sometypeb import SomeTypeB


class PydanticBaseModel(BaseModel):
    someInt: int
    someStr: str
    someTypeA: SomeTypeA
    someTypeB: SomeTypeB

code_to_test.py - Code to Test

from some.path.pydantic_class import PydanticBaseModel


class ClassToTest():
    def test_method(self)
        pydantic_base_model = PydanticBaseModel(
            someInt=0,
            someStr="value",
            someTypeA=<SomeTypeAObj>,
            someTypeB=<SomeTypeBObj>
        )
        [...]

test_code.py - Test Code

import unittest
from mockito import ANY, when


class SomeTypeTest(unittest.TestCase):
    def test_sometype_method(self):
        when(PydanticBaseModel(
            someInt=ANY(),
            someStr=ANY(),
            someTypeA=ANY(),
            someTypeB=ANY()
        )).thenReturn(None)
        [...]

Test Output (Simplified)

(test-project) [username@username-pc test-project]$ pytest -sv ./test_code.py
=================================================================== test session starts ====================================================================

[...]

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

>   ???
E   pydantic.error_wrappers.ValidationError: 4 validation errors for PydanticBaseModel
E   someInt
E     value is not a valid integer (type=type_error.integer)
E   someStr
E     str type expected (type=type_error.str)
E   someTypeA
E     value is not a valid dict (type=type_error.dict)
E   someTypeA
E     value is not a valid dict (type=type_error.dict)

pydantic/main.py:338: ValidationError
================================================================= short test summary info ==================================================================
FAILED test_code.py::SimulacaoComboTest::test_sometype_method - pydantic.error_wrappers.ValidationError: 2 validat...
==================================================================== 1 failed in 0.94s =====================================================================

Thanks!

Eduardo Lucio
  • 1,771
  • 2
  • 25
  • 43

3 Answers3

2

I'm not familiar with mockito, but you look like you're misusing both when, which is used for monkey-patching objects, and ANY(), which is meant for testing values, not for assignment.

The mockito walk-through shows how to use the when function. You use that when you need to mock out some functionality.

The ANY function is a matcher: it's used to match e.g. arguments in function calls.

Here's an example of both of those in action:

If you want os.path.exists to always return True, regardless of the path, you can call:

>>> when(os.path).exists(ANY).thenReturn(True)
>>> os.path.exists("/path/example")
True
>>> os.path.exists("/another/example")
True

Here, ANY in the argument list matches any argument, so os.path.exists will return True regardless of how we call it.

If we only wanted it to return True for a specific path, we would have written instead:

>>> when(os.path).exists(ANY).thenReturn(True)
>>> when(os.path).exists("/another/example").thenReturn(True)
>>> os.path.exists("/path/example")
False
>>> os.path.exists('/another/example')
True

For what you're doing, you don't appear to need either one of these constructs. If you want to test "when I create a PydanticBaseModel, the object returned has the same values I used when constructing it", then you could write:

import unittest

from model import PydanticBaseModel, SomeTypeA, SomeTypeB


class SomeTypeTest(unittest.TestCase):
    def test_sometype_method(self):
        expectedTypeA = SomeTypeA()
        expectedTypeB = SomeTypeB()

        expected = {
            "someInt": 0,
            "someStr": "",
            "someTypeA": expectedTypeA,
            "someTypeB": expectedTypeB,
        }

        model = PydanticBaseModel(
            someInt=0,
            someStr="",
            someTypeA=expectedTypeA,
            someTypeB=expectedTypeB,
        )

        assert model.dict() == expected
larsks
  • 277,717
  • 41
  • 399
  • 399
  • Dude, that wasn't an explanation, it was a lesson ! What if I simply want to bypass the execution of "PydanticBaseModel", so that its call doesn't throw errors? That is, I don't care that "PydanticBaseModel" is executed, only that it does not generate errors in its execution in the test scenario. – Eduardo Lucio Jun 04 '22 at 16:09
  • If you don't care about the value, just call `PydanticBaseModel(...)` without assigning the value to anything and without `assert`-ing anything. Either the test will pass (the function completes without errors), or it will raise an exception, causing the test to fail. – larsks Jun 04 '22 at 16:36
  • Note that I added `.thenReturn(None)` in the call to "PydanticBaseModel" (test_code.py) to better explain what I want. That is, I don't want this class to be instantiated, because the parameters for it in the call are invalid. I just need the "instance" to return "None". – Eduardo Lucio Jun 04 '22 at 16:36
1

Ok, friends!

I found 3 different approaches to mock the instantiation (construction) of a Pydantic (BaseModel) class.

NOTE: I don't know if these are the best ways to approach the problem or even if they are correct. So I ask you to comment!

APPROACH 1 (The best in my view )

import unittest
from unittest import mock


class SomeTypeTest(unittest.TestCase):

    @mock.patch("some.path.pydantic_class.PydanticBaseModel.__init__")
    def test_sometype_method(self, pydantic_base_model):
        pydantic_base_model.return_value = None
        <SOME_PATCH_DEPENDENT_CODE>
        [...]

APPROACH 2

import unittest

from unittest.mock import patch
from some.path.pydantic_class import PydanticBaseModel

class SomeTypeTest(unittest.TestCase):

    def test_sometype_method(self):
        with patch.object(PydanticBaseModel, "__init__", return_value=None):
            <SOME_PATCH_DEPENDENT_CODE>

        [...]

APPROACH 3

import unittest
from unittest.mock import patch


class SomeTypeTest(unittest.TestCase):

    def test_sometype_method(self):
        patcher = patch("some.path.pydantic_class.PydanticBaseModel.__init__", return_value=None)
        patcher.start()
        self.addCleanup(patcher.stop)
        <SOME_PATCH_DEPENDENT_CODE>
        [...]

NOTE: With "addCleanup" you no longer need to keep a reference to the patcher object.
Ref(s).: https://docs.python.org/3/library/unittest.mock.html#patch-methods-start-and-stop


PLUS: I continue to insist that the Mockito approach...

when(PydanticBaseModel).thenReturn(None)

... fails because the "PydanticBaseModel" class uses Pydantic (BaseModel), which seems to be something Mockito is not able to handle.


Thanks!


UPDATE (20220613.2232): While all the answers "effectively" manage to mock the classes, PROBABLY THE CORRECT ANSWER IS THIS...

import unittest
from unittest.mock import patch
from some.path.pydantic_class import PydanticBaseModel


class SomeTypeTest(unittest.TestCase):

    def test_sometype_method(self):
        patcher = patch("some.path.pydantic_class.PydanticBaseModel.__new__", return_value=PydanticBaseModel(**{<INITIALIZATION_DICT_CONTENT>}))
        patcher.start()
        self.addCleanup(patcher.stop)
        <SOME_PATCH_DEPENDENT_CODE>
        [...]

... because this way we can define, under our control, a class instance return (and not just "None").

NOTE: Note that we now use __new__ instead of __init__. For that reason, this is probably the correct answer!

Eduardo Lucio
  • 1,771
  • 2
  • 25
  • 43
0

A Pydantic model attribute can also be mocked by using a unittest.mock.patch with autospec, e.g. in pytest with decorator syntax:

from model import PydanticBaseModel
from unittest import mock


class TestPydanticBaseModel:

    @mock.patch('model.SomeTypeA', autospec=True)
    def test_with_some_type_a(self, mock_type_a):
        PydanticBaseModel(someTypeA=mock_type_a)
        mock_type_a.some_method.assert_called()

Alex M
  • 690
  • 4
  • 18