【问题标题】:Python Mock Process for Unit Testing用于单元测试的 Python 模拟过程
【发布时间】:2013-02-26 15:47:34
【问题描述】:

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

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

初步想法:

  • 我可以使用subprocess.Popen() 打开任何系统进程,但这会遇到一些问题。如果我用来测试的进程也由系统运行,则单元测试可能会错误地通过。此外,单元测试是从命令行运行的,我能想到的任何 Linux 进程都会暂停终端(纳米等)。
  • 我可以启动一个进程并通过其进程 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

【问题讨论】:

  • 只是说:您在这里构建的是集成测试,而不是单元测试;单元测试会查看您项目的一个部分,其他所有内容都被剔除并假设正在工作。
  • 不是您问题的答案-但是...您知道psutil吗?它可能会处理您应用程序中大部分无聊/困难的部分。

标签: python unit-testing process mocking


【解决方案1】:

编辑:这个答案越来越长:),但我原来的一些答案仍然适用,所以我把它留在:)

您的代码与我的原始答案没有太大不同。我的一些想法仍然适用。

当您编写单元测试时,您只想测试您的逻辑。当您使用与操作系统交互的代码时,您通常希望模拟该部分。正如您所发现的,原因是您对这些库的输出没有太多控制权。所以更容易模拟这些调用。

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

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

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

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

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

Linux方法

现在,测试您的 Linux 进程功能并不难。您可以修补 os.listdir 以返回 pid 列表。

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% 的代码覆盖率(因为您的模拟方法不会运行),但这很好。您正在测试您的代码而不是第三方的代码,这很重要。

希望这可能对您有所帮助。我知道它不能回答您的问题,但也许您可以使用它来找出测试代码的最佳方法。

【讨论】:

  • 这绝对是一种有趣且可行的方法来测试我的代码。然而,我遇到的问题是它实际上并没有测试我的代码是否在系统上看到新进程,而是代码是否正确地监视列表中的新项目。对此的代码覆盖率可能很重要,但我还想测试我的代码是否能看到系统上的新进程。非常感谢您的帮助!
  • @alexquinn 你是如何获得 Linux 进程的?您提到您正在监控 /proc fir,那么您是否使用了类似 pinotify 的东西?另外,只是为了好玩,你能告诉我使用EnumProcessModules的行吗?
  • @Carlow 我将发布代码以获取原始问题中的系统进程。
  • 我通过创建线程和轮询代码来监控进程,以根据平台获取系统进程。
  • 您编辑的答案对我来说很有意义。非常感谢您花时间解释模拟这些功能背后的动机以及此类测试的理念。由于对单元测试相当陌生,因此非常感谢这种类型的帮助。
【解决方案2】:

您最初使用 subprocess 的想法很好。只需创建您自己的可执行文件并将其命名为将其标识为测试的东西。也许让它做一些事情,比如睡一会儿。

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

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

【讨论】:

    猜你喜欢
    • 2016-09-20
    • 2022-11-29
    • 2022-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-01
    • 2017-11-15
    • 2019-07-27
    相关资源
    最近更新 更多