Testable python class 这取决于另一个 class?
Testable python class which depends on another class?
我有一个 class C
被 X
使用,我将为其编写单元测试。
class C(metaclass=Singleton): # To be mocked
def __init__(self, a, b):
"""..."""
def f(self, x):
return ...
class X: # to be tested
def __init__(self, c: C=C(...)): # do I need parameter c for testability?
self.c = c
x = X() # self.c will be ready
为了保持相同的可测试性,我是否需要构造函数参数c: C=C(...)
?就
怎么样
class X: # to be tested
def __init__(self): # No parameter c
self.c = C(...)
def g(self):
x = self.c.f(...)
并使用一些方法来“修补”C.f()
?测试代码看起来有何不同?我会用pytest。
注意,将对象实例化为参数的默认值将在不同的调用之间共享同一个对象(参见 this question)。
至于你的问题,如果你真的希望对 X 的调用与 x = X() # self.c will be ready
中的相同,那么你将需要修补你的 X.__init__
使用的 Config
引用.
如果您放宽此限制,即您接受 x = X(c=Config())
代替,那么提供“test double" of your choice (typically a "mock"), and ease testing. It is called dependency injection 并且促进测试是其主要优点之一将非常容易。
编辑:因为你需要模拟C.f
,这里有一个例子:
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()
因为 C
在你的例子中已经存在(它在同一个文件中),所以不可能简单地 mock.patch
,它需要 mock.patch.object
.
此外,可以编写更简单的模拟,或者让 patch
创建一个简单的 Mock
,但对于示例,我更喜欢明确。
我有一个 class C
被 X
使用,我将为其编写单元测试。
class C(metaclass=Singleton): # To be mocked
def __init__(self, a, b):
"""..."""
def f(self, x):
return ...
class X: # to be tested
def __init__(self, c: C=C(...)): # do I need parameter c for testability?
self.c = c
x = X() # self.c will be ready
为了保持相同的可测试性,我是否需要构造函数参数c: C=C(...)
?就
class X: # to be tested
def __init__(self): # No parameter c
self.c = C(...)
def g(self):
x = self.c.f(...)
并使用一些方法来“修补”C.f()
?测试代码看起来有何不同?我会用pytest。
注意,将对象实例化为参数的默认值将在不同的调用之间共享同一个对象(参见 this question)。
至于你的问题,如果你真的希望对 X 的调用与 x = X() # self.c will be ready
中的相同,那么你将需要修补你的 X.__init__
使用的 Config
引用.
如果您放宽此限制,即您接受 x = X(c=Config())
代替,那么提供“test double" of your choice (typically a "mock"), and ease testing. It is called dependency injection 并且促进测试是其主要优点之一将非常容易。
编辑:因为你需要模拟C.f
,这里有一个例子:
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()
因为 C
在你的例子中已经存在(它在同一个文件中),所以不可能简单地 mock.patch
,它需要 mock.patch.object
.
此外,可以编写更简单的模拟,或者让 patch
创建一个简单的 Mock
,但对于示例,我更喜欢明确。