【问题标题】:Mock class instances without calling `__init__` and mock their respective attributes模拟类实例而不调用 __init__ 并模拟它们各自的属性
【发布时间】:2020-06-24 07:32:27
【问题描述】:

我有一个类 MyClass 和一个复杂的 __init__ 函数。

这个类有一个我想测试的方法my_method(self)

my_method 只需要类实例中的属性my_attribute

有没有一种方法可以模拟类实例而不调用__init__,而是设置每个类实例的属性?

我有什么:

# my_class.py

from utils import do_something

class MyClass(object):

   def __init__(self, *args, **kwargs):
       # complicated function which I would like to bypass when initiating a mocked instance class
       pass

   def my_method(self):
      return do_something(self.my_attribute)

我尝试了什么

@mock.patch("my_class.MyClass")
def test_my_method(class_mock, attribute):
   
   instance = class_mock.return_value
   instance.my_attribute = attribute

   example_instance = my_class.MyClass()

   out_my_method = example_instance.my_method()
   # then perform some assertions on `out_my_method`

但是这仍然使用__init__,我希望我们可以绕过或模拟。

【问题讨论】:

  • 我想说把你复杂的东西放到__init__中调用的另一个函数中,你可以简单地模拟它。
  • 一种简单的方法是MyClass.my_method(any_object_with_my_attribute)(即调用类上的方法并提供您自己的“实例”),但您遇到的问题表明问题出在 MyClass 的设计和任何快速修复只是一种解决方法,不能解决问题。你能提供更多背景信息吗?
  • @KlausD。这个想法是有效的,但理想情况下,我会避免改变主代码的结构
  • @jonrsharpe init 函数从磁盘加载文件以及我在另一个单元测试中负责的其他事情。在这种情况下,我想测试每个类方法,而不必实例化一个类实例。答案提供了一种清晰易读的方式来实施您的建议。
  • “我想在不必实例化类实例的情况下测试每个类方法” - 这再次表明设计不佳。例如,您可以将文件的加载拆分为一个类方法,因此实际的 __init__ 只需使用 data - MyClass.from_file("path/to/file")

标签: python unit-testing mocking pytest


【解决方案1】:

正如我在 cmets 中提到的,无需创建实例即可测试单个方法的一种方法是:

MyClass.my_method(any_object_with_my_attribute)

问题在于,与quamrana's answer 中的两个选项一样,我们现在已经扩展了任何未来更改的范围只是因为测试。如果对my_method 的更改需要访问附加属性,我们现在必须同时更改实现其他内容(SuperClassMockMyClass,或者在本例中为any_object_with_my_attribute_and_another_one )。


让我们举一个更具体的例子:

import json


class MyClass:

    def __init__(self, filename):
        with open(filename) as f:
            data = json.load(f)
        self.foo = data.foo
        self.bar = data.bar
        self.baz = data.baz

    def my_method(self):
        return self.foo ** 2

这里是任何需要MyClass 实例的测试。由于__init__ 中的文件访问而痛苦。一个更可测试的实现会将如何访问数据的细节和有效实例的初始化分开:

class MyClass:
    
    def __init__(self, foo, bar, baz):
        self.foo = foo
        self.bar = bar
        self.baz = baz

    def my_method(self):
        return self.foo ** 2

    @classmethod
    def from_json(cls, filename):
        with open(filename) as f:
            data = json.load(f)
        return cls(data.foo, data.bar, data.baz)

您必须将 MyClass("path/to/file") 重构为 MyClass.from_json("path/to/file"),但无论您已经拥有数据(例如在您的测试中),您都可以使用例如MyClass(1, 2, 3) 无需文件即可创建实例(您只需要在from_json 本身的测试中考虑文件)。这使得实例真正需要更加清晰,并且允许引入其他方式来构造实例而不改变接口。

【讨论】:

  • 非常感谢您把它写出来并花时间在这上面。虽然这不是问题的答案,但它很好地解释了为什么不应该出现这个问题。
【解决方案2】:

我至少可以看到两个选项:

  1. 提取一个超类:

    class SuperClass:
        def __init__(self, attribute):
            self.my_attribute = attribute
       def my_method(self):
          return do_something(self.my_attribute)
    
    class MyClass(SuperClass):
    
       def __init__(self, *args, **kwargs):
           super().__init__(attribute)   # I don't know where attribute comes from
           # complicated function which I would like to bypass when initiating a mocked instance class
    

    您的测试可以实例化SuperClass 并调用my_method()

  2. 照原样从MyClass继承并制作您自己的简单__init__()

    class MockMyClass(MyClass):
        def __init__(self, attribute):
            self.my_attribute = attribute
    

    现在您的测试代码可以使用所需属性实例化MockMyClass 并调用my_method()

    例如,您可以编写如下测试

    def test_my_method(attribute):
    
       class MockMyClass(MyClass):
          def __init__(self, attribute):
             self.my_attribute = attribute
    
       out_my_method = MockMyClass(attribute).my_method()
       # perform assertions on out_my_method
    

【讨论】:

  • 我期待使用 Mock 类,但根本没有。我喜欢第二种选择。它干净易读。我在我的用例上对其进行了测试,它可以工作
  • 这两种方法的问题在于未来的变化范围。如果my_method 开始需要其他属性,例如,现在有更多的地方可以编辑只是因为测试
猜你喜欢
  • 2016-04-27
  • 1970-01-01
  • 2016-03-31
  • 1970-01-01
  • 2020-06-11
  • 1970-01-01
  • 1970-01-01
  • 2023-03-15
  • 1970-01-01
相关资源
最近更新 更多