2015-10-06 18 views
11

Ho una classe base che estende unittest.TestCase e voglio applicare la patch alla classe base, in modo tale che le classi che estendono questa classe base applichino anche le patch.Ereditarietà di una classe con patch

Esempio di codice:

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

class TestFunctions(BaseTest): 
     #methods 
     pass 

Patching classe TestFunctions lavora direttamente, ma l'applicazione di patch la classe BaseTest non cambia la funzionalità di some.core.function in TestFunctions.

+2

Questo è probabilmente il luogo in cui vuoi cercare ["metaclasses python" di google (https://www.google.fi/search?q=python+metaclasses) e continuare a leggere finché non capisci come funzionano. Metaclass è ereditato dalle sottoclassi, i decoratori decorano solo la classe su cui sono utilizzati. –

+0

Ah, penso di capire un po 'cosa intendi. Le patch si verificano solo su istanze di classi? – sihrc

+0

No, 'patch' è un decoratore che prende solo la classe direttamente al di sotto e decora quello. Ora qualsiasi sottoclasse non sarà decorata, saranno solo classi normali. I metaclassi controllano il comportamento di come vengono create le classi e quindi possono applicare una patch a una classe quando viene creata per la prima volta. I metaclassi funzionano anche su sottoclassi dopo che è stato impostato il metaclasse della classe base, quindi anche le sottoclassi verranno applicate. –

risposta

7

Qui probabilmente vuoi un metaclasse: una metaclasse semplicemente definisce come viene creata una classe. Per impostazione predefinita, tutte le classi vengono creati utilizzando classe built-in di Python type:

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

Così le classi sono in realtà casi di type. Ora, possiamo sottoclasse type per creare un metaclasse personalizzato (una classe che crea classi):

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

Abbiamo bisogno di controllare la creazione delle nostre classi, quindi vogliamo ignorare il type.__new__ qui, e utilizzare il patch decoratore su tutte le nuove istanze:

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 

E ora è sufficiente impostare la metaclasse utilizzando __metaclass__ = PatchMeta:

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

La questione è questa linea:

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

Quindi attualmente abbiamo sempre decoriamo con argomenti "some.core.function" e mocked_method. Invece si potrebbe fare in modo che esso utilizza gli attributi della classe, in questo modo:

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

e quindi aggiungere patch_args alle classi:

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

Edit: Come @mgilson menzionato nei commenti, patch() modifica i metodi della classe, invece di restituire una nuova classe.A causa di questo, siamo in grado di sostituire il __new__ con questo __init__:

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) 

Quale è più pulita abbastanza indiscutibilmente.

+0

Ah! Sono arrivato a una conclusione simile, ma invece ho esaminato tutte le funzioni iniziate con 'test' e le ho derise. Il tuo sembra più elegante. Ci sono vantaggi/svantaggi per entrambi? – sihrc

+0

È possibile avere più di 1 metaclasse? O ho bisogno di sorta di inizio è? – sihrc

+0

@sihrc Perché vuoi più di nessuno dei metaclassi? Non ho mai incontrato un tale bisogno, potresti fare qualcosa di sbagliato qui. Non penso che ci siano davvero tanti svantaggi. –

7

In genere, preferisco fare questo genere di cose in setUp. Si può fare in modo che la patch venga ripulita dopo il test è completato facendo uso del metodo tearDown (o, in alternativa, la registrazione di un metodo del cerotto stop con addCleanup):

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 

A condizione che si sta abbastanza disciplinato per chiamare sempre super nei metodi sostituiti setUp, dovrebbe funzionare correttamente.

+0

Penso che anche questo abbia i suoi meriti. Non ho mai visto le patch fatte in questo modo. Grazie mille! – sihrc