如何无需手动子类化测试Python抽象类的具体方法?
问题描述
我有一个包含具体方法的Python抽象类:
import abc class AbstractFoo(abc.ABC): def append_something(self, text: str) -> str: return text + self.create_something(len(text)) @abc.abstractmethod def create_something(self, number: int) -> str: raise NotImplementedError() # IMPORTANT: many other abstract methods irrelevant for the test omitted here
由于该类存在大量抽象方法,我希望找到一种无需手动子类化并实现所有抽象方法的方式来测试其中的具体方法,尝试通过mock实例来实现,但两次尝试均失败:
第一次失败尝试
# Without this, `AbstractFoo` cannot be instantiated. AbstractFoo.__abstractmethods__ = frozenset() mock_foo = unittest.mock.Mock(wraps=AbstractFoo()) mock_foo.create_something.return_value = "bar" assert mock_foo.append_something("foo") == "foobar" mock_foo.create_something.assert_called_once_with(3)
尽管尝试mock create_something方法的返回值,但调用mock_foo.append_something("foo")时仍抛出AbstractFoo.create_something中的NotImplementedError。
第二次失败尝试
class MockFoo(AbstractFoo): def create_something(self, number: int) -> str: return "bar" MockFoo.__abstractmethods__ = frozenset() mock_foo = unittest.mock.Mock(wraps=MockFoo()) mock_foo.create_something.side_effect = lambda *args, **kwargs: unittest.mock.DEFAULT assert mock_foo.append_something("foo") == "foobar" mock_foo.create_something.assert_called_once_with(3)
此次出现断言错误:
AssertionError: Expected 'create_something' to be called once. Called 0 times.
问题
- 两次尝试中
mock_foo.create_something的mock均未生效,原因是什么? - 如何在无需手动实现
AbstractFoo所有抽象方法的前提下,测试其append_something方法?
解答
1. 两次尝试mock失效的原因
第一次尝试失败原因
用unittest.mock.Mock(wraps=AbstractFoo())创建mock对象时,append_something实际调用的是被包装的AbstractFoo()实例的方法。你仅清空了__abstractmethods__让实例能被创建,但create_something还是原抽象方法,会抛出NotImplementedError。而mock对象的create_something并未被调用——原方法直接调用自身实例的方法,完全绕过了mock层。
第二次尝试失败原因
你给MockFoo实现了create_something,并用Mock(wraps=MockFoo())包装。调用mock_foo.append_something时,实际执行的是被包装的MockFoo实例的方法,该方法直接调用自身已实现的create_something,根本没用到mock对象上的方法,所以断言检测不到调用记录。
2. 无需实现所有抽象方法的测试方案
这里提供两种简洁可行的方案:
方案一:用patch临时替换实例的抽象方法
临时清空抽象方法集合创建实例,再用patch替换目标方法:
import unittest.mock def test_append_something(): # 保存原抽象方法集合,后续恢复 original_abstracts = AbstractFoo.__abstractmethods__ AbstractFoo.__abstractmethods__ = frozenset() try: foo = AbstractFoo() # 替换实例的create_something方法 with unittest.mock.patch.object(foo, 'create_something', return_value='bar') as mock_create: result = foo.append_something('foo') assert result == 'foobar' mock_create.assert_called_once_with(3) finally: # 恢复原抽象方法集合,避免影响其他测试 AbstractFoo.__abstractmethods__ = original_abstracts
方案二:用create_autospec生成符合接口的mock
create_autospec会自动生成匹配类接口的mock对象,无需手动处理抽象方法:
import unittest.mock def test_append_something(): # 创建符合AbstractFoo接口的mock实例 mock_foo = unittest.mock.create_autospec(AbstractFoo, spec_set=True, instance=True) mock_foo.create_something.return_value = 'bar' result = mock_foo.append_something('foo') assert result == 'foobar' mock_foo.create_something.assert_called_once_with(3)
这个方法最简洁,create_autospec会自动处理抽象方法的约束,同时保证mock的方法调用能被正确捕获。
内容的提问来源于stack exchange,提问作者Chriki

