2013-04-04 1 views
38

Nel mio tentativo di imparare TDD, provando a imparare il test delle unità e l'utilizzo di mock con python. Prendendoci lentamente la mano, ma non sono sicuro se lo sto facendo correttamente. Prevedo: sto stucking usando python 2.4 perché le API del fornitore arrivano come file pyc precompilati a 2.4, quindi sto usando mock 0.8.0 e unittest (non unittest2)Come usare correttamente mock in python con il set-up non più nuovo

Dato questo codice di esempio in 'mymodule.py '

import ldap 

class MyCustomException(Exception): 
    pass 

class MyClass: 
    def __init__(self, server, user, passwd): 
     self.ldap = ldap.initialize(server) 
     self.user = user 
     self.passwd = passwd 

    def connect(self): 
     try: 
      self.ldap.simple_bind_s(self.user, self.passwd) 
     except ldap.INVALID_CREDENTIALS: 
      # do some stuff 
      raise MyCustomException 

Ora nel mio test case file 'test_myclass.py', voglio prendere in giro il LDAP oggetto out. ldap.initialize restituisce ldap.ldapobject.SimpleLDAPObject, quindi ho pensato che sarebbe stato il metodo che avrei dovuto prendere in giro.

import unittest 
from ldap import INVALID_CREDENTIALS 
from mock import patch, MagicMock 
from mymodule import MyClass 

class LDAPConnTests(unittest.TestCase): 
    @patch('ldap.initialize') 
    def setUp(self, mock_obj): 
     self.ldapserver = MyClass('myserver','myuser','mypass') 
     self.mocked_inst = mock_obj.return_value 

    def testRaisesMyCustomException(self): 
     self.mocked_inst.simple_bind_s = MagicMock() 
     # set our side effect to the ldap exception to raise 
     self.mocked_inst.simple_bind_s.side_effect = INVALID_CREDENTIALS 
     self.assertRaises(mymodule.MyCustomException, self.ldapserver.connect) 

    def testMyNextTestCase(self): 
     # blah blah 

mi porta ad un paio di domande:

  1. fa che guardare a destra? :)
  2. È il modo corretto di provare e prendere in giro un oggetto che viene istanziato all'interno della classe che sto testando?
  3. È giusto chiamare il decoratore @patch su setUp o causerà strani effetti collaterali?
  4. Esiste comunque la possibilità di ottenere il mock per sollevare l'eccezione ldap.INVALID_CREDENTIALS senza dover importare l'eccezione nel mio file testcase?
  5. Dovrei usare patch.object() invece e se sì, come?

Grazie.

+1

1-3) sembra che vada bene a me ... 4) 'importazione ldap' invece e impostare' side_effect = ldap.INVALID_CREDENTIALS'? – Chris

+0

Puoi sempre fare lo stesso test ma con oggetti più semplici fatti da te ... – shackra

risposta

36

See: 26.5.3.4. Applying the same patch to every test method

Ha più senso per impostare il patcher in questo modo sulla messa a punto, se si desidera che la patch per essere fatto per tutti i metodi di prova.

+2

Per la stessa cosa per pre-Python3 Mock (ospitato su http://www.voidspace.org.uk/python/mock/), vedi [Applicando la stessa patch ad ogni metodo di prova] (http://www.voidspace.org.uk/python/mock/examples.html#applying-the-same-patch-to-every-test-method). – musiphil

+2

Mi sono appena imbattuto in un problema in cui avevo un mock di livello di classe su una classe di TestCase e presumevo che sarebbe già stato sul posto quando si effettua una chiamata nel metodo 'setUp()'. QUESTO NON È IL CASO; i mock di livello di classe non sono applicati in tempo per l'uso in 'setUp()'. Ho risolto il problema, invece, creando un metodo di supporto che utilizzo in tutti i miei test. Non sono sicuro che questo sia l'approccio migliore, ma funziona. – berto

+0

@berto Se espandi il tuo commento in una risposta, penso che sarà utile. È una soluzione diversa e probabilmente più semplice rispetto alle altre qui. – KobeJohn

4

Se si dispone di molte patch da applicare e si desidera loro di applicarsi a cose inizializzati nei metodi di impostazione anche provare questo:

def setUp(self): 
    self.patches = { 
     "sut.BaseTestRunner._acquire_slot": mock.Mock(), 
     "sut.GetResource": mock.Mock(spec=GetResource), 
     "sut.models": mock.Mock(spec=models), 
     "sut.DbApi": make_db_api_mock() 
    } 

    self.applied_patches = [mock.patch(patch, data) for patch, data in self.patches.items()] 
    [patch.apply for patch in self.applied_patches] 
    . 
    . rest of setup 
    . 


def tearDown(self): 
    patch.stop_all() 
+3

considera l'uso di 'patch.stop_all()' in 'tearDown()'. –

+0

Ho provato questo - sembra che anche i apply_patches debbano essere avviati. Considera una riga come: 'per la patch in self.applied_patches: patch.start()' – F1Rumors

3

Comincerò rispondendo alle vostre domande, e poi ti darò un esempio dettagliato di come interagiscono e setUp().

  1. Non penso che sia corretto, vedere la risposta # 3 per i dettagli.
  2. Sì, la chiamata effettiva alla patch sembra che dovrebbe prendere in giro l'oggetto desiderato.
  3. No, non si desidera quasi mai utilizzare il decoratore @patch() su setUp(). Sei fortunato, perché l'oggetto viene creato in setUp() e non viene mai creato durante il metodo di prova.
  4. Non conosco alcun modo per far sì che un oggetto fittizio sollevi un'eccezione senza importare quell'eccezione nel file del test case.
  5. Non vedo alcuna esigenza per patch.object() qui. Semplicemente ti consente di correggere gli attributi di un oggetto invece di specificare il target come una stringa.

Per espandere la risposta n. 3, il problema è che il decoratore si applica solo mentre la funzione decorata è in esecuzione. Non appena viene restituito setUp(), la patch viene rimossa.Nel tuo caso, funziona, ma scommetto che confonderebbe qualcuno che guarda questo test. Se si desidera realmente che la patch si verifichi durante lo setUp(), suggerirei di utilizzare l'istruzione with per rendere evidente che la patch verrà rimossa.

L'esempio seguente ha due casi di test. TestPatchAsDecorator mostra che la decorazione della classe applicherà la patch durante il metodo di prova, ma non durante lo setUp(). TestPatchInSetUp mostra come è possibile applicare la patch in modo che sia in posizione durante sia setUp() sia il metodo di prova. Chiamando self.addCleanUp() si assicura che la patch venga rimossa durante tearDown().

import unittest 
from mock import patch 


@patch('__builtin__.sum', return_value=99) 
class TestPatchAsDecorator(unittest.TestCase): 
    def setUp(self): 
     s = sum([1, 2, 3]) 

     self.assertEqual(6, s) 

    def test_sum(self, mock_sum): 
     s1 = sum([1, 2, 3]) 
     mock_sum.return_value = 42 
     s2 = sum([1, 2, 3]) 

     self.assertEqual(99, s1) 
     self.assertEqual(42, s2) 


class TestPatchInSetUp(unittest.TestCase): 
    def setUp(self): 
     patcher = patch('__builtin__.sum', return_value=99) 
     self.mock_sum = patcher.start() 
     self.addCleanup(patcher.stop) 

     s = sum([1, 2, 3]) 

     self.assertEqual(99, s) 

    def test_sum(self): 
     s1 = sum([1, 2, 3]) 
     self.mock_sum.return_value = 42 
     s2 = sum([1, 2, 3]) 

     self.assertEqual(99, s1) 
     self.assertEqual(42, s2)