如何模拟作为库的一部分的超类?

时间:2015-01-27 23:19:55

标签: python mocking unit-testing

我在Python中有以下代码:

library.py

class HasSideEffects(object):

    def __init__(self, kittens):
        print 'kittens cry'  # <- side effect

    def launch_nukes(self):
        print 'launching nukes'  # <- side effect

my_code.py

from library import HasSideEffects


class UtilitySubclass(HasSideEffects):

    def party(self):
        self.launch_nukes()
        return 'confetti'  # I want to test that my confetti cannon will work


if __name__ == '__main__':
    x = UtilitySubclass(1)  # oh no! crying kittens :(
    x.party()  # oh no! nukes!

我想进行单元测试,当我致电party时,我会得到confetti。但是,我想避免作为我正在子类化的库类的一部分的所有副作用。更具体地说,我想在单元测试中避免这些副作用,但是在生产代码中需要副作用。

我无法更改library.py中的代码,因为它是一个库,我没有编写代码。显然,我不想仅仅为了便于单元测试而维护一个分叉版本的库。

我一直在试图模仿HasSideEffects超类。我需要在超类中模拟__init__launch_nukes,以便它们不再执行副作用。我在模拟__init__方法时遇到了一些麻烦,显然Python mock库不支持它?

测试party返回confetti的最佳方法是什么,同时避免副作用?

2 个答案:

答案 0 :(得分:3)

也许你非常接近解决方案:

  

我一直在尝试模拟HasSideEffects超类。我需要在超类中模拟 init 和launch_nukes,以便它们不再执行副作用。我在模拟 init 方法时遇到了一些麻烦,显然Python模拟库不支持它?

unittest.mock and legacy mock的设计完全是为了做这种工作。当您需要从库或资源中删除依赖项时,模拟它并由patch替换是非常强大的,尤其是当您应该使用无法更改的遗留代码时。

在您的情况下,您需要修补__init__方法:要做到这一点,您必须考虑两件事

  1. 如果类是子类,并且在构建子类对象时需要修补它,则必须直接修补__init__而不是类定义。
  2. __init__方法必须返回None和你的模拟。
  3. 现在回到你的简单示例:我稍微改变了一下,使测试更加明确。

    <强> library.py

    class HasSideEffects(object):
    
        def __init__(self, kittens):
            raise Exception('kittens cry')  # <- side effect
    
    
        def launch_nukes(self):
            raise Exception('launching nukes')  # <- side effect
    

    <强> my_code.py

    from library import HasSideEffects
    
    class UtilitySubclass(HasSideEffects):
    
        def party(self):
            self.launch_nukes()
            return 'confetti'  # I want to test that my confetti cannon will work
    

    <强> test_my_code.py

    import unittest
    from unittest.mock import patch, ANY
    from my_code import UtilitySubclass
    
    
    class MyTestCase(unittest.TestCase):
        def test_step_by_step(self):
            self.assertRaises(Exception, UtilitySubclass) #Normal implementation raise Exception
            #Pay attention to return_value MUST be None for all __init__ methods
            with patch("library.HasSideEffects.__init__", autospec=True, return_value=None) as mock_init:
                self.assertRaises(TypeError, UtilitySubclass) #Wrong argument: autospec=True let as to catch it
                us = UtilitySubclass("my kittens") #Ok now it works
                #Sanity check: __init__ call?
                mock_init.assert_called_with(ANY, "my kittens") #Use autospec=True inject self as first argument -> use Any to discard it
                #But launch_nukes() was still the original one and it will raise
                self.assertRaises(Exception, us.party)
                with patch("library.HasSideEffects.launch_nukes") as mock_launch_nukes:
                    self.assertEqual("confetti",us.party())
                    # Sanity check: launch_nukes() call?
                    mock_launch_nukes.assert_called_with()
    
        @patch("library.HasSideEffects.launch_nukes")
        @patch("library.HasSideEffects.__init__", autospec=True, return_value=None)
        def test_all_in_one_by_decorator(self, mock_init, mock_launch_nukes):
            self.assertEqual("confetti",UtilitySubclass("again my kittens").party())
            mock_init.assert_called_with(ANY, "again my kittens")
            mock_launch_nukes.assert_called_with()
    
    
    if __name__ == '__main__':
        unittest.main()
    

    请注意,装饰器版本简洁明了。

答案 1 :(得分:1)

这是一个非常棘手的问题。如果您正在模拟的库类很简单,那么您可以将您的功能作为 mixin 提供。

class UtilitySubclassMixin(object):
    def party(self):
        self.launch_nukes()
        return 'confetti'

class UtilitySubclass(library.HasSideEffects, UtilityClassMixin):
    """meaningful docstring."""

现在,为了进行测试,您只需要提供一个具有您的UtililtySubclassMixin所期望的接口的子类(例如,具有launch_nukes方法的接口)。这不是理想的,但是如果你需要模拟一个带有副作用的__init__的库方法,它就是你能做的最好的方法。如果它是所有具有副作用的非魔法方法,unittest.mock可用于直接修补library.HasSideEffects上的方法。

而且,FWIW,这是__init__方法永远不会产生副作用的一个很好的理由: - )。