【问题标题】:How to test an abstract factory如何测试抽象工厂
【发布时间】:2015-09-18 04:30:03
【问题描述】:

我正在尝试在 Python 中使用一个抽象工厂,使用以下 3 个文件进行最低限度的复制:

test_factory.py

from factory import Factory

def test_factory():
    factory = Factory.makeFactory()
    product = factory.makeProduct('Hi there')
    print(product)

if __name__ == '__main__':
    test_factory()

factory.py

from abc import ABCMeta, abstractmethod
from product import ConcreteProduct

class Factory(metaclass=ABCMeta):
    @classmethod
    @abstractmethod
    def makeProduct(cls):
        pass

    @classmethod
    def makeFactory(cls):
        return ConcreteFactory()

class ConcreteFactory(Factory):
    @classmethod
    def makeProduct(cls, message):
        return ConcreteProduct(message)

product.py

class ConcreteProduct(object):
    def __init__(self, message):
        self._message = message
    def __str__(self):
        return self._message

我无法弄清楚如何模拟此代码以验证 ConcreteProduct.__init__ 是否使用适当的值调用。由于测试文件永远不会看到product.py,因此我不确定如何完成此操作,或者是否可能。我怀疑我的设计存在更根本的问题。

【问题讨论】:

  • Factory.makeFactory 应该返回cls(),而不是ConcreteFactory(),以避免对特定子类的任何依赖。
  • @chepner 在这种情况下它将返回一个 Factory 实例,其中包含我拥有的代码。有没有更好的方法来构建它?
  • cls 绑定到调用该方法的任何类,而不是定义该方法的类。
  • 对,我直接从Factory 调用它,所以cls 将是Factory
  • 如果ConcreteFactoryFactory 的唯一子级,那么单独的基类和子类没有多大意义;如果除了ConcreteFactory 之外还有另一个孩子,那么Factory.makeFactory 为什么要返回一个实例呢?通常,工厂函数将使用参数调用以指定使用哪个类来创建实例;一种方法是直接从要实例化的类中调用makeFactory

标签: python python-3.x factory abstract-factory


【解决方案1】:

最简单的方法是patchfactory.ConcreteProduct模块引用。

所以你的测试可以(未测试):

from factory import Factory
from unittest.mock import *

@patch("factory.ConcreteProduct")
def test_factory(mock_product_factory):
    mock_product = mock_product_factory.return_value
    factory = Factory.makeFactory()
    product = factory.makeProduct('Hi there')
    self.assertIs(product, mock_product)
    mock_product_factory.assert_called_with('Hi there')

if __name__ == '__main__':
    test_factory()

如果(且仅当)factory 模块中的ConcreteProduct 引用在您的测试环境中不存在,您可以使用create=True patch 的属性来注入它。

我想指出factory 模块中的ConcreteProduct 引用已经是一个工厂。 Python 中的类是工厂,但它不是类型语言,工厂概念不像 java 那样死板。我来自 Java 背景,即使在 python 中我仍然使用工厂,但是当您应该操纵输入以创建正确的对象时,它们变得非常有用,如果您的工厂方法只是传递给类引用的参数,请考虑删除中间的人。

【讨论】:

  • 这很有帮助。如果您想进一步讨论或有任何建议,请查看我的回答。
  • @anderspitman 我已经更改了答案,以澄清对create 属性的误解。
【解决方案2】:

到目前为止,我已经得到了@Michele dAmico 的回答所引导的解决方案,并且非常接近他的解决方案。

test_factory.py 变为:

from factory import Factory
from unittest.mock import patch

@patch('product.ConcreteProduct.__init__', return_value=None)
def test_factory(mock_init):
    factory = Factory.makeFactory()
    product = factory.makeProduct('Hi there')
    mock_init.assert_called_with('Hi there')


if __name__ == '__main__':
    test_factory()

请注意,我正在修补 product.,而不是 factory.,所以我基本上是在回避 factory.py 并嘲笑我知道它将导入的内容。我不知道我对以这种方式打破封装的感觉如何,但老实说,这就是我对一般嘲笑的感觉。

我更喜欢这个而不是另一个答案,因为它有点短,而且因为根据the mock docs 它可能很危险:

默认情况下,补丁将无法替换不存在的属性。如果 你传入create=True,属性不存在,patch会 在调用修补函数时为您创建属性,并且 之后再次删除它。这对于编写针对 您的生产代码在运行时创建的属性。关闭 默认,因为它可能很危险。打开它你可以写 针对实际上不存在的 API 通过测试!

我当然会对进一步的讨论感兴趣,因为我仍然有一种感觉,我可以学习一些更好的设计方法来使这个更干净。

【讨论】:

  • 如果ConcreteClass 派生自其他类并且不要覆盖它,请注意补丁__init__ 可能很危险(python 不是Java,构造函数可以继承)。此外,使用修补 __init__ 作为最后机会,以防 init 在测试中执行您不想要的操作,例如启动线程或打开远程连接,否则如果可以,请避免它:patch just public 方法是好的规则,如果可以的话,避免修补私有和魔法方法。根据这条规则,您的测试将更加简单和可维护。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多