2015-10-06 17 views
11

Ich habe eine Basisklasse, die unittest.TestCase erweitert, und ich möchte diese Basisklasse patchen, sodass bei Klassen, die diese Basisklasse erweitern, auch die Patches angewendet werden.Vererbung einer gepatchten Klasse

Codebeispiel:

@patch("some.core.function", mocked_method) 
class BaseTest(unittest.TestCase): 
     #methods 
     pass 

class TestFunctions(BaseTest): 
     #methods 
     pass 

Patchen der TestFunctions Klasse direkt funktioniert, aber die BaseTest Klasse Patchen nicht die Funktionalität von some.core.function in TestFunctions ändern.

+2

Dies ist wahrscheinlich, wo Sie google ["Python Metaklassen"] (https://www.google.fi/search?q=python+metaclasses) und lesen Sie weiter, bis Sie verstehen, wie sie funktionieren. Metaklasse wird von den Unterklassen geerbt, Dekoratoren dekorieren nur die Klasse, in der sie verwendet werden. –

+0

Ah, ich glaube ich verstehe was du meinst. Patches treten nur bei Instanzen von Klassen auf? – sihrc

+0

Nein, 'patch' ist ein Dekorateur, der nur die Klasse direkt darunter nimmt und diese verziert. Jetzt werden keine Unterklassen dekoriert, sie werden nur normale Klassen sein. Metaklassen steuern das Verhalten der Klassen und können daher eine Klasse bei ihrer Erstellung patchen. Metaklassen funktionieren auch auf Unterklassen, nachdem die Metaklasse der Basisklasse festgelegt wurde. Daher werden Unterklassen ebenfalls gepatcht. –

Antwort

7

Sie möchten wahrscheinlich eine Metaklasse hier: eine Metaklasse definiert einfach, wie eine Klasse erstellt wird. Standardmäßig werden alle Klassen erstellt Pythons eingebaute Klasse mit type:

>>> class Foo: 
...  pass 
... 
>>> type(Foo) 
<class 'type'> 
>>> isinstance(Foo, type) 
True 

So Klassen sind eigentlich Instanzen type. Jetzt können wir type Unterklasse eine benutzerdefinierte metaclass (eine Klasse, die Klassen erstellt) zu erstellen:

class PatchMeta(type): 
    """A metaclass to patch all inherited classes.""" 

wir die Schaffung unserer Klassen steuern müssen, so wollen wir die type.__new__ hier außer Kraft gesetzt, und verwenden Sie die patch Dekorateur auf allen neuen Instanzen:

class PatchMeta(type): 
    """A metaclass to patch all inherited classes.""" 

    def __new__(meta, name, bases, attrs): 
     cls = type.__new__(meta, name, bases, attrs) 
     cls = patch("some.core.function", mocked_method)(cls) 
     return cls 

Und jetzt stellen Sie einfach die Metaklasse mit __metaclass__ = PatchMeta:

class BaseTest(unittest.TestCase): 
    __metaclass__ = PatchMeta 
    # methods 

Das Problem ist, diese Zeile:

cls = patch("some.core.function", mocked_method)(cls) 

So derzeit wir immer mit Argumenten dekorieren "some.core.function" und mocked_method. Stattdessen könnten Sie machen es so, dass es die Attribute des Klasse verwendet, etwa so:

cls = patch(*cls.patch_args)(cls) 

Und dann patch_args zu Ihren Klassen hinzufügen:

class BaseTest(unittest.TestCase): 
    __metaclass__ = PatchMeta 
    patch_args = ("some.core.function", mocked_method) 

Edit: Wie @mgilson erwähnt In den Kommentaren ändert patch() die Methoden der Klasse anstelle einer neuen Klasse.Aus diesem Grund können wir die __new__ mit diesem __init__ ersetzen:

class PatchMeta(type): 
    """A metaclass to patch all inherited classes.""" 

    def __init__(cls, *args, **kwargs): 
     super(PatchMeta, self).__init__(*args, **kwargs) 
     patch(*cls.patch_args)(cls) 

die ganz unarguably sauberer ist.

+0

Ah! Ich bin gerade zu einem ähnlichen Schluss gekommen, aber stattdessen habe ich alle Funktionen durchgelesen, die mit "Test" begonnen und diese verspottet haben. Deines scheint eleganter. Gibt es Vorteile/Nachteile? – sihrc

+0

Ist es möglich, mehr als 1 Metaklasse zu haben? Oder muss ich es von Anfang an einordnen? – sihrc

+0

@sihrc Warum willst du mehr als keine Metaklasse? Ich bin niemals auf solche Bedürfnisse gestoßen, vielleicht tust du hier etwas falsch. Ich glaube nicht, dass es so viele Nachteile gibt. –

7

Im Allgemeinen bevorzuge ich diese Art der Sache in setUp. Sie können sicherstellen, dass die Patch bereinigt wird nach der Prüfung durch die Nutzung der tearDown Verfahren (oder alternativ die Registrierung eine das Patches stop Methode mit addCleanup) abgeschlossen ist:

class BaseTest(unittest.TestCase): 
     def setUp(self): 
      super(BaseTest, self).setUp() 
      my_patch = patch("some.core.function", mocked_method) 
      my_patch.start() 
      self.addCleanup(my_patch.stop) 

class TestFunctions(BaseTest): 
     #methods 
     pass 

Vorausgesetzt, dass Sie diszipliniert genug sind um immer super in Ihren überschriebenen setUp Methoden aufzurufen, sollte es gut funktionieren.

+0

Ich denke, das hat auch seine Vorzüge. Ich habe noch nie Patches auf diese Weise gesehen. Vielen Dank! – sihrc