2010-09-30 3 views
46

Angenommen, ich habe den folgenden Code in einem Python-Unit-Test genannt:Assert, dass ein Verfahren in einem Python Unit-Test

aw = aps.Request("nv1") 
aw2 = aps.Request("nv2", aw) 

Gibt es eine einfache Möglichkeit, dass ein bestimmtes Verfahren geltend zu machen (in meinem Fall aw.Clear()) wurde während der zweiten Zeile des Tests aufgerufen? z.B. so etwas wie das ist es:

#pseudocode: 
assertMethodIsCalled(aw.Clear, lambda: aps.Request("nv2", aw)) 

Antwort

80

Ich benutze Mock dafür:

from mock import patch 
from PyQt4 import Qt 

@patch.object(Qt.QMessageBox, 'aboutQt') 
def testShowAboutQt(self, mock): 
    self.win.actionAboutQt.trigger() 
    self.assertTrue(mock.called) 

Für Ihren Fall, es könnte wie folgt aussehen:

import mock 

def testClearWasCalled(self): 
    aw = aps.Request("nv1") 
    with patch.object(aw, 'Clear') as mock: 
     aw2 = aps.Request("nv2", aw) 

    mock.assert_called_with(42) # or mock.assert_called_once_with(42) 

Mock unterstützt eine ganze Reihe von nützlichen Funktionen, einschließlich der Möglichkeiten, ein Objekt oder ein Modul zu patchen , sowie überprüfen, dass das Richtige aufgerufen wurde, usw.

Caveat emptor! (Käufer Vorsicht!)

Wenn Sie assert_called_with vertippen (zu assert_called_once oder assert_called_wiht) Ihr Test noch laufen kann, wie Mock wird denken, dies ist eine Funktion verspottet und glücklich geht zusammen, es sei denn, Sie autospec=true verwenden. Für weitere Informationen lesen Sie assert_called_once: Threat or Menace.

+3

+1 für diskret erleuchtend meine Welt mit dem wunderbaren Mock-Modul. –

+0

@RonCohen: Ja, es ist ziemlich erstaunlich und wird immer besser. :) – Macke

+1

Während der Verwendung von Mock ist definitiv der Weg zu gehen, würde ich davon abraten mit assert_called_once, mit einfach nicht existiert :) – FelixCQ

4

Sie verspotten können aw.Clear, entweder manuell oder ein Test-Framework wie pymox verwenden. Manuell, können Sie es so etwas wie dies mit tun würde:

class MyTest(TestCase): 
    def testClear(): 
    old_clear = aw.Clear 
    clear_calls = 0 
    aw.Clear = lambda: clear_calls += 1 
    aps.Request('nv2', aw) 
    assert clear_calls == 1 
    aw.Clear = old_clear 

pymox verwenden, würden Sie es wie folgt tun:

class MyTest(mox.MoxTestBase): 
    def testClear(): 
    aw = self.m.CreateMock(aps.Request) 
    aw.Clear() 
    self.mox.ReplayAll() 
    aps.Request('nv2', aw) 
+0

ich auch diesen Ansatz mag, obwohl ich old_clear noch will genannt werden. Dies macht es offensichtlich, was vor sich geht. –

7

Ja, kann ich Ihnen die Umrisse geben, aber meine Python ist ein etwas rostig und ich bin zu beschäftigt, um im Detail zu erklären.

Grundsätzlich müssen Sie einen Proxy in dem Verfahren stellen, die das Original rufen, zB:

class fred(object): 
    def blog(self): 
    print "We Blog" 


class methCallLogger(object): 
    def __init__(self, meth): 
    self.meth = meth 

    def __call__(self, code=None): 
    self.meth() 
    # would also log the fact that it invoked the method 

#example 
f = fred() 
f.blog = methCallLogger(f.blog) 

Diese StackOverflow answer über aufrufbare kann Ihnen helfen, die oben verstehen.

Im Detail:

Obwohl die Antwort akzeptiert wurde, aufgrund der interessanten Diskussion mit Glenn und ein paar Minuten frei hat, wollte ich auf meine Antwort vergrößern:

# helper class defined elsewhere 
class methCallLogger(object): 
    def __init__(self, meth): 
    self.meth = meth 
    self.was_called = False 

    def __call__(self, code=None): 
    self.meth() 
    self.was_called = True 

#example 
class fred(object): 
    def blog(self): 
    print "We Blog" 

f = fred() 
g = fred() 
f.blog = methCallLogger(f.blog) 
g.blog = methCallLogger(g.blog) 
f.blog() 
assert(f.blog.was_called) 
assert(not g.blog.was_called) 
+0

nett. Ich habe eine Anrufzählung zu methCallLogger hinzugefügt, damit ich darauf bestehen kann. –

+0

Dies über die gründliche, in sich geschlossene Lösung, die ich zur Verfügung gestellt habe? Ernst? –

+0

@Glenn Ich bin sehr neu in Python - vielleicht ist deine eine besser - ich verstehe das alles noch nicht. Ich werde ein bisschen Zeit damit verbringen, es auszuprobieren. –

13

I mir ist nichts bekannt. Es ist ziemlich einfach zu implementieren:

class assertMethodIsCalled(object): 
    def __init__(self, obj, method): 
     self.obj = obj 
     self.method = method 

    def called(self, *args, **kwargs): 
     self.method_called = True 
     self.orig_method(*args, **kwargs) 

    def __enter__(self): 
     self.orig_method = getattr(self.obj, self.method) 
     setattr(self.obj, self.method, self.called) 
     self.method_called = False 

    def __exit__(self, exc_type, exc_value, traceback): 
     assert getattr(self.obj, self.method) == self.called, 
      "method %s was modified during assertMethodIsCalled" % self.method 

     setattr(self.obj, self.method, self.orig_method) 

     # If an exception was thrown within the block, we've already failed. 
     if traceback is None: 
      assert self.method_called, 
       "method %s of %s was not called" % (self.method, self.obj) 

class test(object): 
    def a(self): 
     print "test" 
    def b(self): 
     self.a() 

obj = test() 
with assertMethodIsCalled(obj, "a"): 
    obj.b() 

Dies erfordert, dass das Objekt selbst self.b nicht ändern wird, was fast immer der Fall ist.

+0

Ich sagte mein Python war rostig, obwohl ich meine Lösung getestet habe, um sicherzustellen, dass es funktioniert :-) Ich verinnerlichte Python vor Version 2.5, tatsächlich habe ich nie 2.5 für irgendeine signifikante Python verwendet, da wir bei 2.3 für lib-Kompatibilität einfrieren mussten. Bei der Überprüfung Ihrer Lösung fand ich http://effbot.org/zone/python-statement.htm als eine schöne klare Beschreibung. Ich würde demütig vorschlagen, dass mein Ansatz kleiner aussieht und möglicherweise einfacher anzuwenden ist, wenn Sie mehr als einen Punkt der Protokollierung wünschen, anstatt verschachtelte "mit" s. Ich möchte wirklich, dass Sie erklären, ob es irgendwelche besonderen Vorteile von Ihnen gibt. –

+0

@Andy: Ihre Antwort ist kleiner, weil sie unvollständig ist: Sie testet die Ergebnisse nicht, sie stellt die ursprüngliche Funktion nach dem Test nicht wieder her, so dass Sie das Objekt weiter verwenden können und Sie müssen den Code wiederholt schreiben all das jedes Mal, wenn Sie einen Test schreiben. Die Anzahl der Zeilen des Support-Codes ist nicht wichtig. Diese Klasse wird in einem eigenen Testmodul verwendet, nicht in einem Docstring - dies erfordert beim tatsächlichen Test eine oder zwei Zeilen Code. –

+0

+1 für die Verwendung eines Kontext-Manager, nette Idee! –

10

Ja, wenn Sie Python 3.3+ verwenden. Sie können die eingebaute unittest.mock Methode zum Aufrufen der Methode verwenden. Für Python 2.6+ benutzen Sie den Rollbackport , was dasselbe ist.

Hier ist ein kurzes Beispiel in Ihrem Fall:

from unittest.mock import MagicMock 
aw = aps.Request("nv1") 
aw.Clear = MagicMock() 
aw2 = aps.Request("nv2", aw) 
assert aw.Clear.called 
Verwandte Themen