【问题标题】:Dependency Injection in Unit Test with PythonPython 单元测试中的依赖注入
【发布时间】:2017-02-22 16:33:41
【问题描述】:

我正在学习python

我想知道是否有一种机制可以将一个对象(在我的例子中是一个假对象)“注入”到正在测试的类中,而无需在 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(.....)

在我的测试中,我想“注入”对象“引擎”而不将其传递给构造函数。我尝试了几个选项(例如:@patch ...)但没有成功。

【问题讨论】:

    标签: python mocking python-unittest


    【解决方案1】:

    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(.....)
    

    你也可以通过 stub 类绕过构造函数

    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 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 框架,只是不要默认为 prod 服务。
    【解决方案2】:

    在使用没有任何 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:控制反转是一个简单的概念,即框架带走对某些东西的控制,通常是为了提供抽象并带走样板代码。当您使用模板类时,这就是 IoC。 IoC 的好坏完全取决于框架如何实现它。

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

    当我处理更大的应用程序时,过于简单的方法不再适用:样板代码过多,并且缺少 DI 的主要优势:只需更改某事的实现,并使其反映在所有依赖于的类中它。如果您的应用程序的许多部分关心如何初始化某个依赖项并且您更改此初始化或想要更改类,则您必须逐个更改它。使用 DI 框架会更容易。

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

    在座右铭 Dependency Injection for Humans™ 下,它看起来是这样的:

    # 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):
        ...
    

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

      • “我想“注入”对象“引擎”而不将它传递给构造函数”。没看懂,有什么不明白的?其他语言的其他单元测试框架(例如:java)可以在没有 setter 或 costructor 的情况下将对象注入其他对象。就是这样
      • 不清楚的是,您同时要求以不涉及使用猴子修补、继承或传递任何值的方式同时注入“MyBusinessClassFake”和“引擎”,并且必须也是自动的。如果@patch 是不可能的,你确定你不能只做以下事情吗? unit = MyBusinessClass() unit.__engine = RepperEngine()
      • 抱歉有误会。我在哪里说过我不想使用@patch?补丁对我很好......我刚刚说过我无法使用@patch ......
      猜你喜欢
      • 2021-06-19
      • 1970-01-01
      • 2017-04-01
      • 2017-02-07
      • 1970-01-01
      • 2021-05-29
      • 2018-03-03
      相关资源
      最近更新 更多