Testable python class which depends on another class?
Beware, instantiating an object as default value for a parameter will share the same object between deifferent calls (see this question).
As for your question, if you really to want the call to X the same, as in x = X() # self.c will be ready
then you will need to patch the Config
reference that your X.__init__
uses.
If you relax this constraint, meaning that you accept to do x = X(c=Config())
instead, it will be very easy to provide the "test double" of your choice (typically a "mock"), and ease testing. It is called dependency injection and facilitating testing is one of its main advantages.
EDIT: as you need to mock C.f
, here is an example :
class C:
def __init__(self, a, b):
self.a = a
self.b = b
def f(self, z): # <-- renamed to `z` to avoid confusion with the `x` instance of class `X`
print(f"method F called with {z=}, {self.a=} and {self.b=}")
return z + self.a + self.b
class X:
def __init__(self, c: C = C(a=1, b=2)):
self.c = c
# /!\ to do some mocking, I had to `pip install pytest_mock` (version 3.6.1) as `pytest` provides no mocking support
def test_my_class_X_with_C_mock(mocker): # the `mocker` parameter is a fixture, it is automatically provided by the pytest runner
x = X() # create the X as always
assert x.c.f(z=3) == 6 # prints "method F called with z=3, self.a=1 and self.b=2"
def my_fake_function_f(z): # create a function to call instead of the original `C.f` (optional)
print(f"mock intercepted call, {z=}")
return 99
my_mock = mocker.Mock( # define a mock
**{"f.side_effect": my_fake_function_f}) # which has an `f`, which calls the fake function when called (side effect)
mocker.patch.object(target=x, attribute="c", new=my_mock) # patch : replace `x.c` with `my_mock` !
assert x.c.f(z=3) == 99 # prints "mock intercepted call, z=3"
def main():
# do the regular thing
x = X()
print(x.c.f(z=3)) # prints "method F called with z=3, self.a=1 and self.b=2" and the result "6"
# then do the tests
import pytest
pytest.main(["-rP", __file__]) # run the test, similar to the shell command `pytest <filename.py>`, and with `-rP` show the print
# it passes with no error !
if __name__ == "__main__":
main()
Because C
already exists (it is in the same file) in your example, it was not possible to simply mock.patch
, it was required to mock.patch.object
.
Also, it is possible to write simpler mocks, or just let patch
create a plain Mock
, but for the example I preferred to be explicit.