用于单元测试的Python模拟过程

时间:2013-02-26 15:47:34

标签: python unit-testing process mocking

背景
我目前正在用Python编写流程监控工具(Windows和Linux)并实现单元测试覆盖。进程监视器挂钩到Windows上的Windows API函数EnumProcesses,并监视Linux上的/ proc目录以查找当前进程。然后将进程名称和进程ID写入可供单元测试访问的日志。

问题:
当我对监控行为进行单元测试时,我需要一个启动和终止的过程。如果有一种(跨平台?)方式来启动和终止我可以唯一命名的假系统进程(并在单元测试中跟踪其创建),我会很高兴。

初步想法:

  • 我可以使用subprocess.Popen()打开任何系统进程,但这会遇到一些问题。如果我用于测试的过程也由系统运行,则单元测试可能会错误地通过。此外,单元测试是从命令行运行的,我可以想到的任何Linux进程都可以暂停终端(nano等)。
  • 我可以启动一个进程并按进程ID跟踪它,但我不确定如何在不暂停终端的情况下执行此操作。

这些只是初步测试的想法和观察结果,如果有人能证明我在这两点上的错误,我会很高兴。

我正在使用Python 2.6.6。

修改
获取所有Linux进程ID:

try:
    processDirectories = os.listdir(self.PROCESS_DIRECTORY)
except IOError:
    return []
return [pid for pid in processDirectories if pid.isdigit()]

获取所有Windows进程ID:

import ctypes, ctypes.wintypes

Psapi = ctypes.WinDLL('Psapi.dll')
EnumProcesses = self.Psapi.EnumProcesses
EnumProcesses.restype = ctypes.wintypes.BOOL

count = 50
while True:
    # Build arguments to EnumProcesses
    processIds = (ctypes.wintypes.DWORD*count)()
    size = ctypes.sizeof(processIds)
    bytes_returned = ctypes.wintypes.DWORD()
    # Call enum processes to find all processes
    if self.EnumProcesses(ctypes.byref(processIds), size, ctypes.byref(bytes_returned)):
        if bytes_returned.value &lt size:
            return processIds
       else:
            # We weren't able to get all the processes so double our size and try again
            count *= 2
    else:
        print "EnumProcesses failed"
        sys.exit()

Windows code is from here

2 个答案:

答案 0 :(得分:5)

编辑:这个答案变得越来越长了:),但我的一些原始答案仍然适用,所以我将其留在:)

您的代码与我原来的答案没有太大区别。我的一些想法仍然适用。

在编写单元测试时,您只想测试您的逻辑。当您使用与操作系统交互的代码时,通常需要模拟该部分。正如您所发现的那样,原因是您无法控制这些库的输出。因此,模拟这些调用会更容易。

在这种情况下,有两个与系统交互的库:os.listdirEnumProcesses。由于你没有写它们,我们可以很容易地伪造它们以返回我们需要的东西。在这种情况下是一个列表。

但是等等,你在评论中提到:

  

“我遇到的问题是它确实没有测试   我的代码正在看到系统上的新进程,而不是   代码正在监视列表中的新项目。“

问题是,我们不需要来测试实际上监视系统上的进程的代码,因为它是第三方代码。我们需要测试的是代码逻辑处理返回的进程。因为那是你写的代码。我们测试列表的原因是因为这就是你的逻辑所做的。 os.listirEniumProcesses返回一个pid列表(分别为数字字符串和整数),您的代码将作用于该列表。

我假设你的代码在一个类中(你在代码中使用self)。我也假设他们在自己的方法中被隔离(你使用return)。所以这将是我最初的建议,除了实际的代码:) Idk如果它们在同一个类或不同的类中,但它并不重要。

Linux方法

现在,测试Linux进程函数并不困难。您可以修补os.listdir以返回pids列表。

def getLinuxProcess(self):
    try:
        processDirectories = os.listdir(self.PROCESS_DIRECTORY)
    except IOError:
        return []
    return [pid for pid in processDirectories if pid.isdigit()]

现在进行测试。

import unittest
from fudge import patched_context
import os
import LinuxProcessClass # class that contains getLinuxProcess method

def test_LinuxProcess(self):
    """Test the logic of our getLinuxProcess.

       We patch os.listdir and return our own list, because os.listdir
       returns a list. We do this so that we can control the output 
       (we test *our* logic, not a built-in library's functionality).
    """

    # Test we can parse our pdis
    fakeProcessIds = ['1', '2', '3']
    with patched_context(os, 'listdir', lamba x: fakeProcessIds):
        myClass = LinuxProcessClass()
        ....
        result = myClass.getLinuxProcess()

        expected = [1, 2, 3]
        self.assertEqual(result, expected)

    # Test we can handle IOERROR
    with patched_context(os, 'listdir', lamba x: raise IOError):
        myClass = LinuxProcessClass()
        ....
        result = myClass.getLinuxProcess()

        expected = []
        self.assertEqual(result, expected)

    # Test we only get pids
    fakeProcessIds = ['1', '2', '3', 'do', 'not', 'parse']
    .....

Windows方法

测试Window的方法有点棘手。我要做的是以下几点:

def prepareWindowsObjects(self):
    """Create and set up objects needed to get the windows process"
    ...
    Psapi = ctypes.WinDLL('Psapi.dll')
    EnumProcesses = self.Psapi.EnumProcesses
    EnumProcesses.restype = ctypes.wintypes.BOOL

    self.EnumProcessses = EnumProcess
    ...

def getWindowsProcess(self):

    count = 50
    while True:
       .... # Build arguments to EnumProcesses and call enun process
       if self.EnumProcesses(ctypes.byref(processIds),...
       ..
       else:
           return []

我将代码分成两种方法,以便于阅读(我相信你已经这样做了)。这是一个棘手的部分,EnumProcesses正在使用指针,它们不容易使用。另一件事是,我不知道如何使用Python中的指针,所以我无法告诉你一个简单的方法来模拟出来= P

可以告诉你的是不测试它。你的逻辑非常小。除了增加count的大小之外,该函数中的所有其他内容都是创建指针将使用的空间EnumProcesses。也许你可以添加一个限制计数大小,但除此之外,这种方法简短而且甜蜜。它返回Windows进程,仅此而已。正是我在原评论中所要求的:)

所以请保持这种方法。不要测试它。但请确保使用getWindowsProcessgetLinuxProcess的任何内容都按照我的原始建议进行模拟。

希望这更有意义:)如果它不让我知道,也许我们可以进行聊天会话或进行视频通话等。

原始回答

我不确定如何做你要问的问题,但每当我需要测试依赖于某些外力(外部库,popen或在这种情况下的进程)的代码时,我会模拟掉那些部分。

现在,我不知道你的代码是如何构建的,但也许你可以这样做:

def getWindowsProcesses(self, ...):
   '''Call Windows API function EnumProcesses and
      return the list of processes
   '''
   # ... call EnumProcesses ...
   return listOfProcesses

def getLinuxProcesses(self, ...):
   '''Look in /proc dir and return list of processes'''
   # ... look in /proc ...
   return listOfProcessses

这两种方法只做一件事,获取进程列表。对于Windows,它可能只是对该​​API的调用,对于Linux只是读取/ proc目录。这就是全部,仅此而已。处理流程的逻辑将转移到其他地方。这使得这些方法非常容易模拟,因为它们的实现只是返回列表的API调用。

您的代码可以轻松调用它们:

def getProcesses(...):
   '''Get the processes running.'''
   isLinux = # ... logic for determining OS ...
   if isLinux:
      processes = getLinuxProcesses(...)
   else:
      processes = getWindowsProcesses(...)
   # ... do something with processes, write to log file, etc ...

在测试中,您可以使用模拟库,例如Fudge。你嘲笑这两种方法,以返回你期望返回的内容。

这样您就可以测试您的逻辑,因为您可以控制结果。

from fudge import patched_context
...

def test_getProcesses(self, ...):

     monitor = MonitorTool(..)

     # Patch the method that gets the processes. Whenever it gets called, return
     # our predetermined list.
     originalProcesses = [....pids...]
     with patched_context(monitor, "getLinuxProcesses", lamba x: originalProcesses):
         monitor.getProcesses()
         # ... assert logic is right ...


     # Let's "add" some new processes and test that our logic realizes new 
     # processes were added.
     newProcesses = [...]
     updatedProcesses = originalProcessses + (newProcesses) 
     with patched_context(monitor, "getLinuxProcesses", lamba x: updatedProcesses):
         monitor.getProcesses()
         # ... assert logic caught new processes ...


     # Let's "kill" our new processes and test that our logic can handle it
     with patched_context(monitor, "getLinuxProcesses", lamba x: originalProcesses):
         monitor.getProcesses()
         # ... assert logic caught processes were 'killed' ...

请记住,如果您以这种方式测试代码,则不会获得100%的代码覆盖率(因为您的模拟方法不会运行),但这很好。你正在测试你的代码,而不是第三方,这才是最重要的。

希望这可以帮到你。我知道它没有回答你的问题,但也许你可以用它来找出测试代码的最佳方法。

答案 1 :(得分:0)

您最初使用子流程的想法很好。只需创建自己的可执行文件,并将其命名为将其标识为测试内容。也许让它做一些像睡眠一样的事情。

或者,您实际上可以使用多处理模块。我没有在windows中使用python,但你应该能够从你创建的Process对象中获取进程识别数据:

p = multiprocessing.Process(target=time.sleep, args=(30,))
p.start()
pid = p.getpid()