在Python中使用mock实现干运行模式

4 投票
2 回答
6448 浏览
提问于 2025-04-18 06:41

假设你有一个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%可靠的解决方案,特别是如果你要包装的代码是第三方的,并且可能会有所变化。但至少如果你有一个很大的类,而又不想重写所有方法,这个方法可以帮到你。

希望这能对某些人有所帮助。

撰写回答