[英]Python Mock Process for Unit Testing
背景:
我目前正在用Python編寫過程監視工具(Windows和Linux)並實現單元測試范圍。 進程監視器掛接到Windows上的Windows API函數EnumProcesses上,並監視Linux上的/ proc目錄以查找當前進程。 然后,將過程名稱和過程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 < 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()
編輯:這個答案越來越長:),但是我的某些原始答案仍然適用,所以我將其留在:)
您的代碼與我的原始答案沒什么不同。 我的一些想法仍然適用。
當你寫單元測試,你只想測試你的邏輯。 當您使用與操作系統交互的代碼時,通常希望將其模擬出來。 原因是,您發現對這些庫的輸出沒有太多控制權。 因此,模擬這些調用更加容易。
在這種情況下,有兩個與系統交互的庫: os.listdir
和EnumProcesses
。 由於您沒有編寫它們,因此我們可以輕松地偽造它們以返回我們所需的內容。 在這種情況下是列表。
但是,等等,您在評論中提到:
“但是,我遇到的問題是,它確實不能測試我的代碼是否正在系統上看到新的進程,而是可以正確地監視列表中的新項目。”
事實是,我們不需要測試實際上監視系統上進程的代碼,因為它是第三方代碼。 我們需要測試的是,您的代碼邏輯處理了返回的進程 。 因為那是您編寫的代碼。 我們測試列表的原因是因為這就是您的邏輯。 os.listir
和EniumProcesses
返回EniumProcesses
列表(分別為數字字符串和整數),並且您的代碼在該列表上起作用。
我假設您的代碼在類內(您在代碼中使用self
)。 我還假設它們被隔離在自己的方法中(您正在使用return
)。 因此,這將是我最初建議的內容,但實際代碼除外:) Idk,如果它們屬於同一類或不同類,但這並不重要。
現在,測試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']
.....
測試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進程,僅此而已。 正是我在原始評論中要求的內容:)
因此,請不要使用該方法。 不要測試。 但是請確保,按照我最初的建議,所有使用getWindowsProcess
和getLinuxProcess
東西getLinuxProcess
被getLinuxProcess
掉。
希望這更有意義:)如果沒有讓我知道,也許我們可以進行聊天或進行視頻通話。
原始答案
我不確定要怎么做,但是每當我需要測試依賴於外部力量(外部庫,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%的代碼覆蓋率(因為不會運行模擬方法),但這很好。 您要測試的是代碼而不是第三方的代碼,這很重要。
希望這可以為您提供幫助。 我知道它不能回答您的問題,但是也許您可以使用它來找出測試代碼的最佳方法。
您最初使用子流程的想法是一個好主意。 只需創建您自己的可執行文件,並將其命名為可識別為測試對象的名稱即可。 也許讓它做一些類似睡眠的事情。
或者,您實際上可以使用多處理模塊。 我在Windows中很少使用python,但是您應該能夠從所創建的Process對象中獲取過程標識數據:
p = multiprocessing.Process(target=time.sleep, args=(30,))
p.start()
pid = p.getpid()
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.