在Python中使用mock实现干运行模式
假设你有一个Python模块,这个模块在某些时候会调用一些会产生副作用的方法,比如写文件。现在你想要一个“干跑”模式,也就是让它只是假装在做事情,但实际上什么都不做。那我们该如何实现这个干跑功能呢?这个功能可能会在不同的方法中出现。
import os.path
class Foo
def ReadSomething(self):
with open('/some/path', 'r') as f:
print f.read()
def WriteSomething(self, content):
if not os.path.isfile('/some/path'):
with open('/some/path', 'w') as f:
f.write(content)
我想到了使用unittest.mock
库来动态地替换所有外部API,具体做法是根据一个像干跑这样的变量来决定。这大概是这样的:
@DryRunPatcher('open')
@DryRunPatcher(os.path, 'isfile', return_value=True)
def WriteSomething(self, content, mock_isfile, mock_open):
然后,DryRunPatcher可能会是这样的:
def DryRunPatcher(*patch_args, **patch_kwargs):
def Decorator(func):
def PatchIfDrunRun(*args, **kwargs):
self_arg = args[0] # assuming it's a method
if self_arg._dry_run_mode:
with mock.patch.object(*patch_args, **patch_kwargs):
return func(*args, **kwargs)
else:
return func(*args, **kwargs)
return PatchIfDryRun
return Decorator
上面的代码可能不能直接用,但你明白我的意思。不过问题是,我觉得mock这个东西主要是用来单元测试的。那还有什么其他方法可以用来替换外部API,以实现干跑模式呢?我希望方法本身不需要知道干跑模式的存在,也不想在每个有副作用的调用上都加上额外的处理。
2 个回答
0
我不太确定我是否理解你想要的内容,但我们可以试试看:
代码:
def fake(state=False):
def wrapper(function):
def fakefunc(self, *args, **kwargs):
print("I'm doing nothing!")
if state:
return fakefunc
return function
return wrapper
class Foo:
@fake(True)
def write_something(self, content):
if not os.path.isfile('/some/path'):
with open('/some/path', 'w') as f:
f.write(content)
演示:
foo = Foo()
foo.write_something('hello fake!')
输出:
I'm doing nothing!
1
最近我遇到了类似的需求:我有一个REST API客户端,用于某个服务,支持一些基本的增删改查操作。我希望我的脚本能在“干运行”模式下执行,这样用户可以查看日志,了解如果用某些参数执行程序会发生什么。此外,有些方法是安全的,可以执行(比如递归读取某些对象),但有些方法则应该避免执行。
我看到你提到的关于模拟方法的建议,让我想到了一个不错的解决方案:你可以创建一个代理类,在这个类里决定哪些方法需要执行,哪些方法需要“模拟”。通过模拟,你可以选择要做什么:要么只是打印一条消息跳过,要么在安全的地方(比如临时目录)执行类似的操作。
这看起来可能是这样的:
#!/usr/bin/env python
class Foo(object):
def __init__(self, some_parameter):
self._some_parameter = some_parameter
def read_something(self):
print 'Method `read_something` from class Foo: {}'.format(self._some_parameter)
def write_something(self, content):
print 'Method `write_something` from class Foo: {} {}'.format(self._some_parameter, content)
class FooDryRunProxy(Foo):
def __init__(self, some_parameter, dry_run=False):
super(FooDryRunProxy, self).__init__(some_parameter=some_parameter)
self._dry_run = dry_run
def __getattribute__(self, name):
attr = object.__getattribute__(self, name)
is_dry = object.__getattribute__(self, '_dry_run')
if is_dry and hasattr(attr, '__call__') and name.startswith('write_'):
def dry_run_method(*args, **kwargs):
func_args = list()
if args:
func_args.extend(str(a) for a in args)
if kwargs:
func_args.extend('%s=%s' % (k, v) for k, v in kwargs.items())
print("[DRY-RUN] {name}({func_args})".format(name=name, func_args=', '.join(func_args)))
return dry_run_method
else:
return attr
if __name__ == '__main__':
foo_normal = FooDryRunProxy(some_parameter='bar')
foo_normal.read_something()
foo_normal.write_something('got-something-?')
foo_dry_run = FooDryRunProxy(some_parameter='bar', dry_run=True)
foo_dry_run.read_something()
foo_dry_run.write_something('got-something-?')
这是它的输出:
Method `read_something` from class Foo: bar
Method `write_something` from class Foo: bar got-something-?
Method `read_something` from class Foo: bar
[DRY-RUN] write_something(got-something-?)
当然,这并不是一个100%可靠的解决方案,特别是如果你要包装的代码是第三方的,并且可能会有所变化。但至少如果你有一个很大的类,而又不想重写所有方法,这个方法可以帮到你。
希望这能对某些人有所帮助。