Testable python class 这取决于另一个 class?

Testable python class which depends on another class?

我有一个 class CX 使用,我将为其编写单元测试。

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,但对于示例,我更喜欢明确。