8

背景:
私は現在、Pythonでプロセス監視ツール(WindowsおよびLinux)を作成しており、単体テストカバレッジを実装しています。プロセスモニターは、WindowsではWindows API関数EnumProcessesにフックし、Linuxでは/procディレクトリを監視して現在のプロセスを見つけます。次に、プロセス名とプロセスIDがログに書き込まれ、単体テストにアクセスできます。

質問:
監視動作を単体テストする場合、開始および終了するプロセスが必要です。私が一意に名前を付けることができる(そして単体テストでその作成を追跡する)偽のシステムプロセスを開始および終了する(クロスプラットフォーム?)方法があればいいのにと思います。

最初のアイデア:

  • subprocess.Popen()を使用して任意のシステムプロセスを開くことができますが、これにはいくつかの問題が発生します。テストに使用しているプロセスがシステムによっても実行されている場合、単体テストは誤って合格する可能性があります。また、単体テストはコマンドラインから実行され、私が考えることができるすべてのLinuxプロセスはターミナル(nanoなど)を一時停止します。
  • プロセスを開始し、そのプロセスIDで追跡することはできますが、ターミナルを一時停止せずにこれを行う方法が正確にはわかりません。

これらは最初のテストからの単なる考えと観察であり、誰かがこれらの点のいずれかで私が間違っていることを証明できれば、私はそれが大好きです。

Python2.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コードはここから

4

2 に答える 2

7

編集:この答えは長くなっています:)、しかし私の元の答えのいくつかはまだ当てはまるので、私はそれを残します:)

あなたのコードは私の元の答えとそれほど変わりません。私の考えのいくつかはまだ当てはまります。

単体テストを作成するときは、ロジックのみをテストする必要があります。オペレーティングシステムと相互作用するコードを使用する場合、通常はその部分をモックアウトする必要があります。その理由は、ご存知のように、これらのライブラリの出力をあまり制御できないためです。したがって、これらの呼び出しをモックする方が簡単です。

この場合、システムと相互作用している2つのライブラリがあります:os.listdirEnumProcesses。あなたがそれらを書いていなかったので、私たちは簡単にそれらを偽造して必要なものを返すことができます。この場合はリストです。

しかし、待ってください、あなたが言及したあなたのコメントで:

「しかし、私が抱えている問題は、コードがシステム上で新しいプロセスを認識していることを実際にテストするのではなく、コードがリスト内の新しいアイテムを正しく監視していることです。」

重要なのは、システム上のプロセスを実際に監視するコードはサードパーティのコードであるため、テストする必要がないということです。テストする必要があるのは、コードロジックが返されたプロセスを処理することです。それがあなたが書いたコードだからです。リストをテストしている理由は、それがロジックが実行しているためです。pidのリスト(それぞれ数値文字列と整数)を返し、コードはそのリストに作用します。os.listirEniumProcesses

私はあなたのコードがクラスの中にあると仮定しています(あなたは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 []

読みやすくするために、コードを2つのメソッドに分けました(すでにこれを行っていると思います)。ここで注意が必要なのEnumProcessesは、ポインターを使用することであり、それらを操作するのは簡単ではありません。もう1つは、Pythonでポインターを操作する方法がわからないため、それをモックアウトする簡単な方法を説明できなかったことです= P

私があなたに言うことができるのは、単にそれをテストしないことです。そこにあるあなたの論理はごくわずかです。のサイズを大きくすることに加えて、countその関数の他のすべては、EnumProcessesポインタが使用するスペースを作成しています。カウントサイズに制限を追加できるかもしれませんが、それ以外は、この方法は短くて甘いです。Windowsプロセスのみを返します。私が元のコメントで求めていたもの:)

したがって、その方法はそのままにしておきます。テストしないでください。ただし、私の最初の提案に従って、使用してモックアウトされるgetWindowsProcessものはすべてあることを確認してください。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

これらの2つのメソッドは、プロセスのリストを取得するという1つのことだけを実行します。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などのモックライブラリを使用できます。これらの2つのメソッドをモックアウトして、期待どおりの結果を返します。

このようにして、結果がどうなるかを制御できるため、ロジックをテストします。

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%のコードカバレッジが得られないことに注意してください。ただし、これは問題ありません。重要なのは、サードパーティではなくコードをテストしていることです。

うまくいけば、これはあなたを助けることができるかもしれません。私はそれがあなたの質問に答えないことを知っています、しかし多分あなたはあなたのコードをテストするための最良の方法を理解するためにこれを使うことができます。

于 2013-02-26T21:36:18.327 に答える
0

サブプロセスを使用するというあなたの最初のアイデアは良いものです。独自の実行可能ファイルを作成し、それをテスト対象として識別する名前を付けるだけです。たぶん、しばらく寝るようなことをさせてください。

または、実際にマルチプロセッシングモジュールを使用することもできます。私はWindowsでPythonをあまり使用していませんが、作成したProcessオブジェクトからプロセス識別データを取得できるはずです。

p = multiprocessing.Process(target=time.sleep, args=(30,))
p.start()
pid = p.getpid()
于 2014-03-18T14:45:58.960 に答える