Python 单元测试中的依赖注入

Dependency Injection in Unit Test with Python

我在学习python

我想知道是否有一种机制可以 "inject" 将一个对象(在我的例子中是一个假对象)添加到 class 被测对象中,而无需在 costructor/setter 中显式添加它.

## source file
class MyBusinessClass():
    def __init__(self):
        self.__engine = RepperEngine()

    def doSomething(self):
        ## bla bla ...
        success

## test file
## fake I'd like to inkject
class MyBusinessClassFake():
   def __init__(self):
      pass

def myPrint(self) :
    print ("Hello from Mock !!!!")

class Test(unittest.TestCase):

    ## is there an automatic mechanism to inject MyBusinessClassFake 
    ## into MyBusinessClass without costructor/setter?
    def test_XXXXX_whenYYYYYY(self):

        unit = MyBusinessClass()
        unit.doSomething()
        self.assertTrue(.....)

在我的测试中,我想 "inject" 对象 "engine" 而不是在构造函数中传递它。我尝试了几个选项(例如:@patch ...)但没有成功。

您的问题似乎有些不清楚,但是没有什么可以阻止您使用 class 继承来覆盖原始方法。在这种情况下,导数 class 将如下所示:

class MyBusinessClassFake(MyBusinessClass):
   def __init__(self):
      pass

Python 不需要 IOC。这是一个 Pythonic 方法。

class MyBusinessClass(object):
    def __init__(self, engine=None):
        self._engine = engine or RepperEngine() 
        # Note: _engine doesn't exist until constructor is called.

    def doSomething(self):
        ## bla bla ...
        success

class Test(unittest.TestCase):

    def test_XXXXX_whenYYYYYY(self):
        mock_engine = mock.create_autospec(RepperEngine)
        unit = MyBusinessClass(mock_engine)
        unit.doSomething()
        self.assertTrue(.....)

你也可以去掉 class 来绕过构造器

class MyBusinessClassFake(MyBusinessClass):
   def __init__(self):  # we bypass super's init here
      self._engine = None

然后在您的设置中

def setUp(self):
  self.helper = MyBusinessClassFake()

现在在您的测试中您可以使用上下文管理器。

def test_XXXXX_whenYYYYYY(self):
  with mock.patch.object(self.helper, '_engine', autospec=True) as mock_eng:
     ...

如果你想在不使用构造函数的情况下注入它,那么你可以将它添加为 class 属性。

class MyBusinessClass():
    _engine = None
    def __init__(self):
        self._engine = RepperEngine() 

现在绕过 __init__:

class MyBusinessClassFake(MyBusinessClass):
   def __init__(self):
      pass

现在您可以简单地赋值:

unit = MyBusinessClassFake()
unit._engine = mock.create_autospec(RepperEngine)

在使用没有任何 DI 自动装配框架的 Python 和使用 Spring 的 Java 多年后,我开始意识到简单的 Python 代码通常不需要框架对于没有自动装配的依赖注入(自动装配是 Guice 和 Spring 都在 Java 中做的),即,只做这样的事情就足够了:

def foo(dep = None):  # great for unit testing!
    self.dep = dep or Dep()  # callers can not care about this too
    ...

这是纯粹的依赖注入(非常简单),但没有神奇的框架为您自动注入它们(即自动装配),也没有控制反转。

与@Dan 不同,我不同意 Python 不需要 IoC:控制反转是一个简单的概念,即框架剥夺了对某些事物的控制,通常是为了提供抽象并去除样板代码。当您使用模板 classes 时,这就是 IoC。 IoC 的好坏完全取决于框架如何实现它。

也就是说,依赖注入是一个不需要 IoC 的简单概念。自动装配 DI 可以。

当我处理更大的应用程序时,简单化的方法不再有效:样板代码太多,DI 的关键优势缺失:一次更改某物的实现并在所有应用程序中反映出来classes 取决于它。如果您的应用程序的许多部分都关心如何初始化某个依赖项,并且您更改了此初始化或想要更改 classes,则您将不得不逐个更改它。使用 DI 框架会更容易。

所以我想出了 injectable 一个微型框架,它不会让人感觉非 pythonic,但会首先提供 class 依赖注入自动装配。

根据座右铭 人类依赖注入™ 这就是它的样子:

# some_service.py
class SomeService:
    @autowired
    def __init__(
        self,
        database: Autowired(Database),
        message_brokers: Autowired(List[Broker]),
    ):
        pending = database.retrieve_pending_messages()
        for broker in message_brokers:
            broker.send_pending(pending)
# database.py
@injectable
class Database:
    ...
# message_broker.py
class MessageBroker(ABC):
    def send_pending(messages):
        ...
# kafka_producer.py
@injectable
class KafkaProducer(MessageBroker):
    ...
# sqs_producer.py
@injectable
class SQSProducer(MessageBroker):
    ...