在Python中拦截subprocess.Popen调用

时间:2011-03-02 11:30:47

标签: python testing mocking functional-testing

我正在为遗留的Python脚本编写功能测试,以便我可以对其进行单行更改,而不会因为恐惧而陷入瘫痪。 ;)

有问题的脚本使用 subprocess.Popen 调用wget(1)来下载XML文件,然后对其进行解析:

def download_files():
    os.mkdir(FEED_DIR)
    os.chdir(FEED_DIR)

    wget_process = Popen(
        ["wget", "--quiet", "--output-document", "-", "ftp://foo.com/bar.tar"],
        stdout=PIPE
    )
    tar_process = Popen(["tar", "xf", "-"], stdin=wget_process.stdout)
    stdout, stderr = tar_process.communicate()

显然,最好修改脚本以使用HTTP库而不是执行wget,但正如我所说,它是一个遗留脚本,所以我需要保持我的变化最小化并且绝对专注于业务要求,与获取XML文件的方式无关。

对我来说,明显的解决方案是拦截对 subprocess.Popen 的调用并返回我自己的测试XML。 Intercept method calls in Python演示如何使用 setattr 执行此操作,但我必须遗漏一些内容:

Python 2.6.6 (r266:84292, Sep 15 2010, 16:22:56) 
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import subprocess
>>> object.__getattribute__(subprocess, 'Popen')
<class 'subprocess.Popen'>
>>> attr = object.__getattribute__(subprocess, 'Popen')
>>> hasattr(attr, '__call__')
True
>>> def foo(): print('foo')
... 
>>> foo
<function foo at 0x7f8e3ced3c08>
>>> foo()
foo
>>> setattr(subprocess, '__call__', foo)
>>> getattr(subprocess, '__call__')
<function foo at 0x7f8e3ced3c08>
>>> subprocess.Popen([ r"tail", "-n 1", "x.txt" ], stdout = subprocess.PIPE)
<subprocess.Popen object at 0x7f8e3ced9cd0>
>>> tail: cannot open `x.txt' for reading: No such file or directory

正如您所看到的,正在调用真正的 subprocess.Popen ,尽管属性设置正确(至少对于我未经训练的眼睛)。这只是在交互式Python中运行它的结果,或者我应该期望将这种代码放入我的测试脚本中得到相同的结果:

class MockProcess:
  def __init__(self, output):
    self.output = output

  def stderr(): pass
  def stdout(): return self.output

  def communicate():
    return stdout, stderr


# Runs script, returning output
#
def run_agent():
  real_popen = getattr(subprocess.Popen, '__call__')
  try:
    setattr(subprocess.Popen, '__call__', lambda *ignored: MockProcess('<foo bar="baz" />')
    )
    return real_popen(['myscript.py'], stdout = subprocess.PIPE).communicate()[0]
  finally:
    setattr(subprocess.Popen, '__call__', real_popen)

3 个答案:

答案 0 :(得分:4)

我的方法有几个问题:

我没有意识到args在Python中是神奇的,也不是我需要kwargs。

我正在替换subprocess.Popen.__call__,当我应该替换subprocess.Popen本身时。

最重要的是,替换Popen显然只会影响当前进程,而不是我的代码想要为脚本执行的新进程。新的run_agent方法应如下所示:

def run_agent():
  real_popen = getattr(subprocess, 'Popen')
  try:
    setattr(subprocess, 'Popen', lambda *args, **kwargs: MockProcess('<foo bar="baz" />')
    imp.load_module(
      MY_SCRIPT.replace('.py', '').replace('.', '_'),
      file(SCRIPT_DIR),
      MY_SCRIPT,
      ('.py', 'r', imp.PY_SOURCE)
    )
  finally:
    setattr(subprocess.Popen, '__call__', real_popen)

我的互动会话中有一个拼写错误。它应该是:

Python 2.6.6 (r266:84292, Sep 15 2010, 16:22:56) 
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import subprocess
>>> setattr(subprocess, 'Popen', lambda *args, **kwargs: [1,2])
>>> subprocess.Popen([1], stdout=1)
[1, 2]

答案 1 :(得分:3)

当然,FlexMock的Python版本是更好的选择!

import subprocess
from cStringIO import StringIO
from flexmock import flexmock

def run_agent():
  flexmock(subprocess).should_receive('Popen').and_return(
      StringIO(''), StringIO('<foo bar="baz" />')
  )

答案 2 :(得分:1)

您是否在测试脚本失败时设置subprocess.__call__而不是subprocess.Popen.__call__