2011-12-07 15 views
34

Sto scoprendo che sto usando molti gestori di contesto in Python. Tuttavia, Ho testato una serie di cose che li utilizzano, e spesso mi viene la necessità della seguente:In Python, c'è un buon idioma per usare i gestori di contesto in setup/teardown

class MyTestCase(unittest.TestCase): 
    def testFirstThing(self): 
    with GetResource() as resource: 
     u = UnderTest(resource) 
     u.doStuff() 
     self.assertEqual(u.getSomething(), 'a value') 

    def testSecondThing(self): 
    with GetResource() as resource: 
     u = UnderTest(resource) 
     u.doOtherStuff() 
     self.assertEqual(u.getSomething(), 'a value') 

Quando questo arriva a molte prove, questo è chiaramente sta per diventare noioso, così nello spirito di SPOT/ASCIUTTO (singolo punto di verità/non ripetersi), vorrei refactoring quei bit nei metodi di prova setUp() e tearDown().

Tuttavia, cercando di fare che ha portato a questa bruttezza:

def setUp(self): 
    self._resource = GetSlot() 
    self._resource.__enter__() 

    def tearDown(self): 
    self._resource.__exit__(None, None, None) 

Ci deve essere un modo migliore per fare questo. Idealmente, nel setUp()/tearDown() senza bit ripetitivi per ogni metodo di prova (posso vedere come la ripetizione di un decoratore su ciascun metodo potrebbe farlo).

Modifica: Considerare l'oggetto undertest come interno e l'oggetto GetResource come una terza parte (cosa che non stiamo cambiando).

Ho rinominato GetSlot in GetResource qui-questo è più generale del caso specifico, in cui i gestori di contesto sono il modo in cui l'oggetto è destinato a entrare in uno stato bloccato e fuori.

+1

Non capisco il problema con i metodi 'setUp' /' tearDown', mi sembra perfetto. Suppongo che un'alternativa sarebbe quella di creare un decoratore che usi l'istruzione 'with' e applicarlo automaticamente a tutti i metodi, ma sarebbe più un lavoro senza un reale vantaggio. – interjay

+1

Suppongo sia che io veda i metodi "__" come metodi privati ​​e "magici" che non dovrebbero essere chiamati esplicitamente. Tuttavia, dato che questo è in un contesto di test, forse questo sarà sufficiente. –

+1

Il setup e il teardown sono il pulitore dei due. Penso che GetSlot dovrebbe avere l'API corretta da utilizzare senza il gestore di contesto. Il fatto che tu stia cercando di trovare il modo più pulito per farlo dimostra che GetSlot ha bisogno di lavoro. A meno che GetSlot non sia il tuo codice, nel qual caso prendo tutto indietro. –

risposta

24

Che ne dici di sostituire unittest.TestCase.run() come illustrato di seguito? Questo approccio non richiede di chiamare metodi privati ​​o di fare qualcosa per ogni metodo, che è ciò che l'interrogante voleva.

from contextlib import contextmanager 
import unittest 

@contextmanager 
def resource_manager(): 
    yield 'foo' 

class MyTest(unittest.TestCase): 

    def run(self, result=None): 
     with resource_manager() as resource: 
      self.resource = resource 
      super(MyTest, self).run(result) 

    def test(self): 
     self.assertEqual('foo', self.resource) 

unittest.main() 

Questo approccio permette anche passando l'istanza TestCase al contesto manager, se si desidera modificare l'istanza TestCase lì.

+0

Mi piace - è una soluzione semplice. –

+0

Penso che la risposta non sia chiara. Spiega meglio che il metodo test() è eseguito all'interno del contesto e che doStuff() o doOtherStuff() può essere eseguito prima di affermare qualcosa. –

+0

, in questo modo, 'unittest' valuterà l'occorrenza di un'eccezione all'interno del gestore di contesto' __enter__' o '__exit__', nello stesso modo in cui valuterà l'occorrenza di un'eccezione in' setUp' o 'tearDown'. metodi, rispettivamente? – n611x007

5

Il problema con il chiamare __enter__ e __exit__ come hai fatto, non è che lo hai fatto: possono essere chiamati al di fuori di una dichiarazione with. Il problema è che il tuo codice non prevede di chiamare correttamente il metodo __exit__ dell'oggetto se si verifica un'eccezione.

Quindi, il modo per farlo è avere un decoratore che avvolgerà la chiamata al metodo originale in una istruzione with. Una breve metaclasse può applicare il decoratore in modo trasparente a tutti i metodi di nome test * nella classe -

# -*- coding: utf-8 -*- 

from functools import wraps 

import unittest 

def setup_context(method): 
    # the 'wraps' decorator preserves the original function name 
    # otherwise unittest would not call it, as its name 
    # would not start with 'test' 
    @wraps(method) 
    def test_wrapper(self, *args, **kw): 
     with GetSlot() as slot: 
      self._slot = slot 
      result = method(self, *args, **kw) 
      delattr(self, "_slot") 
     return result 
    return test_wrapper 

class MetaContext(type): 
    def __new__(mcs, name, bases, dct): 
     for key, value in dct.items(): 
      if key.startswith("test"): 
       dct[key] = setup_context(value) 
     return type.__new__(mcs, name, bases, dct) 


class GetSlot(object): 
    def __enter__(self): 
     return self 
    def __exit__(self, *args, **kw): 
     print "exiting object" 
    def doStuff(self): 
     print "doing stuff" 
    def doOtherStuff(self): 
     raise ValueError 

    def getSomething(self): 
     return "a value" 

def UnderTest(*args): 
    return args[0] 

class MyTestCase(unittest.TestCase): 
    __metaclass__ = MetaContext 

    def testFirstThing(self): 
     u = UnderTest(self._slot) 
     u.doStuff() 
     self.assertEqual(u.getSomething(), 'a value') 

    def testSecondThing(self): 
     u = UnderTest(self._slot) 
     u.doOtherStuff() 
     self.assertEqual(u.getSomething(), 'a value') 

unittest.main() 

(ho incluso anche implementazioni finte di "GetSlot" ei metodi e le funzioni nel vostro esempio in modo che io stesso potrei testare la decoratore e metaclasse suggerisco in questa risposta)

+1

Riferendosi ai documenti python: http://docs.python.org/library/unittest.html#unittest.TestCase.tearDown, tearDown viene chiamato dopo ogni test, anche quando sono stati generati casi di errore ed eccezioni (e setUp prima di ogni test). MetaClass/MetaContext entrano e escono di sicuro solo quando MyTestCase viene eseguito come un intero test case, non le singole unità. Questo potrebbe significare che ci sono interazioni tra i test. –

+0

@DannyStaple: come per il mio codice: avvolgo ogni singolo metodo di prova in un decoratore che chiama il testo all'interno di un 'with'statement - sia l'entrata che l'uscita vengono eseguite ad ogni esecuzione del test. Inserire alcune dichiarazioni di stampa sulla funzione test_wrapper e vedere di persona. Come per il tuo codice originale, è bello sapere che verrà chiamato '.__ exit__'. Technicalle '.__ exit__' dovrebbe essere passato informazioni sull'eccezione in questione, ma sono d'accordo che questo non dovrebbe essere un problema nella maggior parte dei casi. – jsbueno

+0

Ah, vedo - il tuo colpire gli oggetti dettati e applicare il contesto a ciascun elemento che inizia con "test", che sarebbero i metodi di test. Il decoratore @wraps è utile sapere -Ho avuto il problema di perdere il nome dei metodi quando li ho confezionati prima. –

2

Direi che dovresti separare il test del gestore di contesto dal test della classe Slot. Si potrebbe anche usare un oggetto fittizio simulando l'interfaccia di inizializzazione/finalizzazione dello slot per testare l'oggetto gestore di contesto e quindi testare separatamente l'oggetto slot.

from unittest import TestCase, main 

class MockSlot(object): 
    initialized = False 
    ok_called = False 
    error_called = False 

    def initialize(self): 
     self.initialized = True 

    def finalize_ok(self): 
     self.ok_called = True 

    def finalize_error(self): 
     self.error_called = True 

class GetSlot(object): 
    def __init__(self, slot_factory=MockSlot): 
     self.slot_factory = slot_factory 

    def __enter__(self): 
     s = self.s = self.slot_factory() 
     s.initialize() 
     return s 

    def __exit__(self, type, value, traceback): 
     if type is None: 
      self.s.finalize_ok() 
     else: 
      self.s.finalize_error() 


class TestContextManager(TestCase): 
    def test_getslot_calls_initialize(self): 
     g = GetSlot() 
     with g as slot: 
      pass 
     self.assertTrue(g.s.initialized) 

    def test_getslot_calls_finalize_ok_if_operation_successful(self): 
     g = GetSlot() 
     with g as slot: 
      pass 
     self.assertTrue(g.s.ok_called) 

    def test_getslot_calls_finalize_error_if_operation_unsuccessful(self): 
     g = GetSlot() 
     try: 
      with g as slot: 
       raise ValueError 
     except: 
      pass 

     self.assertTrue(g.s.error_called) 

if __name__ == "__main__": 
    main() 

Questo rende più semplice codice, impedisce preoccupazione miscelazione e consente di riutilizzare il manager contesto senza dover codificare in molti luoghi.

+0

Penso che il mocking sia probabilmente la soluzione qui, quindi ti darò un +1 per questo. Il codice originale (in un certo numero di casi - ho generalizzato la domanda un po ') ti ha permesso di costruire qualcosa, ma non è stato bloccato fino a quando non entra nel contesto. Nota che nella domanda originale - né il contesto, né lo slot sono il SUT (soggetto sotto test) - sono risorse. –

+0

Tornando a questo ora, penso che prendere in giro la risorsa sia esattamente ciò che farei ora, visto che guardando il codice precedente, come hai sottolineato, la risorsa non è affatto sotto test. Potresti non avere alcun effetto collaterale che la risorsa potrebbe avere usando. Mi piacerebbe prendere in giro solo l'interfaccia di risorse richiesta dal SUT e potrei persino creare aiutanti per costruire la risorsa fittizia se ne avessi bisogno. –

+0

Questa risposta lascia aperta la domanda su come applicare un gestore di contesto in modo DRY ai test in una classe di TestCase. – cjerdonek

7

Manipolare i gestori di contesto in situazioni in cui non si desidera un'istruzione with per pulire le cose se tutte le acquisizioni di risorse sono riuscite è uno dei casi d'uso che è stato progettato per gestire contextlib.ExitStack().

Per esempio (usando addCleanup() invece di un'implementazione personalizzata tearDown()):

def setUp(self): 
    with contextlib.ExitStack() as stack: 
     self._resource = stack.enter_context(GetResource()) 
     self.addCleanup(stack.pop_all().close) 

Questo è l'approccio più robusto, dal momento che gestisce correttamente l'acquisizione di molteplici risorse:

def setUp(self): 
    with contextlib.ExitStack() as stack: 
     self._resource1 = stack.enter_context(GetResource()) 
     self._resource2 = stack.enter_context(GetOtherResource()) 
     self.addCleanup(stack.pop_all().close) 

Qui, se GetOtherResource() fallisce, la prima risorsa verrà ripulita immediatamente dall'istruzione with, mentre se riesce, la chiamata pop_all() rimanderà la pulizia fino alla pulitura registrata f corse di unction.

Se sai che stai sempre e solo andando ad avere una risorsa da gestire, è possibile saltare l'istruzione with:

def setUp(self): 
    stack = contextlib.ExitStack() 
    self._resource = stack.enter_context(GetResource()) 
    self.addCleanup(stack.close) 

Tuttavia, questo è un altro errore di bit prona, dal momento che se si aggiungono più risorse per lo stack senza prima passare alla versione basata sull'istruzione, le risorse allocate correttamente potrebbero non essere prontamente ripulite se le successive acquisizioni di risorse falliscono.

Si può anche scrivere qualcosa di paragonabile con un tearDown() un'implementazione personalizzata salvando un riferimento alla pila risorsa sul banco di prova:

def setUp(self): 
    with contextlib.ExitStack() as stack: 
     self._resource1 = stack.enter_context(GetResource()) 
     self._resource2 = stack.enter_context(GetOtherResource()) 
     self._resource_stack = stack.pop_all() 

def tearDown(self): 
    self._resource_stack.close() 
+0

+1 Questo è decisamente più bello che ignorare 'run'. L'unico svantaggio è che il gestore di contesto non può elaborare eccezioni quando viene chiuso in questo modo. –

2

pytest infissi sono molto vicini alla tua idea/stile, e consentire esattamente quello che vuoi:

import pytest 
from code.to.test import foo 

@pytest.fixture(...) 
def resource(): 
    with your_context_manager as r: 
     yield r 

def test_foo(resource): 
    assert foo(resource).bar() == 42 
+0

Questa è la soluzione con cui ho finito. –

Problemi correlati