unittest.mock
— komma igång¶
Tillagd i version 3.3.
Använda Mock¶
Mock Patching-metoder¶
Vanliga användningsområden för Mock
-objekt inkluderar:
Patching-metoder
Inspelning av metodanrop på objekt
Du kanske vill ersätta en metod på ett objekt för att kontrollera att den anropas med rätt argument av en annan del av systemet:
>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>
När vår mock har använts (real.method
i det här exemplet) har den metoder och attribut som gör att du kan göra påståenden om hur den har använts.
Anteckning
I de flesta av dessa exempel är klasserna Mock
och MagicMock
utbytbara. Eftersom MagicMock
är den mer kapabla klassen är det förnuftigt att använda den som standard.
När mocken har anropats sätts dess attribut called
till True
. Ännu viktigare är att vi kan använda metoden assert_called_with()
eller assert_called_once_with()
för att kontrollera att den anropades med rätt argument.
Detta exempel testar att ett anrop till ProductionClass().method
resulterar i ett anrop till metoden something
:
>>> class ProductionClass:
... def method(self):
... self.something(1, 2, 3)
... def something(self, a, b, c):
... pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)
Mock för metodanrop på ett objekt¶
I det förra exemplet patchade vi en metod direkt på ett objekt för att kontrollera att den anropades korrekt. Ett annat vanligt användningsfall är att skicka ett objekt till en metod (eller någon del av systemet som testas) och sedan kontrollera att det används på rätt sätt.
Den enkla ProductionClass
nedan har en closer
metod. Om den anropas med ett objekt så anropar den close
på det.
>>> class ProductionClass:
... def closer(self, something):
... something.close()
...
Så för att testa det måste vi skicka in ett objekt med en close
-metod och kontrollera att den anropades korrekt.
>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()
Vi behöver inte göra något arbete för att tillhandahålla metoden ”close” på vår mock. Åtkomst till close skapar den. Så om ’close’ inte redan har anropats kommer åtkomst till den i testet att skapa den, men assert_called_with()
kommer att ge upphov till ett felaktigt undantag.
Mocking av klasser¶
Ett vanligt användningsfall är att mocka ut klasser som instansieras av din kod som testas. När du patchar en klass ersätts den klassen med en mock. Instanser skapas genom att anropa klassen. Detta innebär att du kommer åt ”mock-instansen” genom att titta på returvärdet för den mockade klassen.
I exemplet nedan har vi en funktion some_function
som instansierar Foo
och anropar en metod på den. Anropet till patch()
ersätter klassen Foo
med en mock. Instansen Foo
är resultatet av anropet av mocken, så den konfigureras genom att modifiera mock return_value
.
>>> def some_function():
... instance = module.Foo()
... return instance.method()
...
>>> with patch('module.Foo') as mock:
... instance = mock.return_value
... instance.method.return_value = 'the result'
... result = some_function()
... assert result == 'the result'
Namnge dina mockar¶
Det kan vara bra att ge dina mockar ett namn. Namnet visas i mockens repr och kan vara till hjälp när mocken visas i felmeddelanden i tester. Namnet sprids också till attribut eller metoder i mocken:
>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>
Spåra alla samtal¶
Ofta vill man spåra mer än ett enda anrop till en metod. Attributet mock_calls
registrerar alla anrop till barnattribut till mock - och även till deras barn.
>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]
Om du gör ett påstående om mock_calls
och några oväntade metoder har anropats, så kommer påståendet att misslyckas. Detta är användbart eftersom du inte bara hävdar att de anrop du förväntade dig har gjorts, du kontrollerar också att de gjordes i rätt ordning och utan ytterligare anrop:
Du använder objektet call
för att konstruera listor som kan jämföras med mock_calls
:
>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True
Parametrar till anrop som returnerar mocks registreras dock inte, vilket innebär att det inte är möjligt att spåra nästlade anrop där de parametrar som används för att skapa ancestors är viktiga:
>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True
Ange returvärden och attribut¶
Att ställa in returvärdena på ett mock-objekt är trivialt enkelt:
>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3
Naturligtvis kan du göra samma sak för metoder på mock:
>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3
Returvärdet kan också ställas in i konstruktören:
>>> mock = Mock(return_value=3)
>>> mock()
3
Om du behöver en attributinställning på din mock är det bara att göra det:
>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3
Ibland vill man simulera en mer komplex situation, som till exempel mock.connection.cursor().execute("SELECT 1")
. Om vi vill att det här anropet ska returnera en lista måste vi konfigurera resultatet av det nästlade anropet.
Vi kan använda call
för att konstruera uppsättningen anrop i ett ”kedjeanrop” som detta för att enkelt kunna hävda det i efterhand:
>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True
Det är anropet till .call_list()
som förvandlar vårt anropsobjekt till en lista med anrop som representerar de kedjade anropen.
Utlösande av undantag med mocks¶
Ett användbart attribut är side_effect
. Om du anger detta till en undantagsklass eller -instans kommer undantaget att uppstå när mocken anropas.
>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
Bieffektsfunktioner och iterabler¶
side_effect
kan också ställas in på en funktion eller en iterabel. Användningsfallet för side_effect
som en iterabel är när din mock kommer att anropas flera gånger och du vill att varje anrop ska returnera ett annat värde. När du ställer in side_effect
till en iterabel returnerar varje anrop till mocken nästa värde från iterabeln:
>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6
För mer avancerade användningsfall, som att dynamiskt variera returvärdena beroende på vad mocken anropas med, kan side_effect
vara en funktion. Funktionen kommer att anropas med samma argument som mock. Det som funktionen returnerar är det som anropet returnerar:
>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
... return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2
Mocka asynkrona iteratorer¶
Sedan Python 3.8 har AsyncMock
och MagicMock
stöd för att mocka Asynkrona Iteratorer genom __aiter__
. Attributet return_value
för __aiter__
kan användas för att ställa in de returvärden som ska användas för iteration.
>>> mock = MagicMock() # AsyncMock also works here
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
... return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]
Mocka asynkron kontexthanterare¶
Sedan Python 3.8 har AsyncMock
och MagicMock
stöd för att mocka Asynkrona kontexthanterare genom __aenter__
och __aexit__
. Som standard är __aenter__
och __aexit__
AsyncMock
instanser som returnerar en asynkron funktion.
>>> class AsyncContextManager:
... async def __aenter__(self):
... return self
... async def __aexit__(self, exc_type, exc, tb):
... pass
...
>>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here
>>> async def main():
... async with mock_instance as result:
... pass
...
>>> asyncio.run(main())
>>> mock_instance.__aenter__.assert_awaited_once()
>>> mock_instance.__aexit__.assert_awaited_once()
Skapa en mock från ett befintligt objekt¶
Ett problem med överdriven användning av mocking är att det kopplar dina tester till implementeringen av dina mocks snarare än din riktiga kod. Anta att du har en klass som implementerar en viss metod
. I ett test för en annan klass tillhandahåller du en mock av detta objekt som också tillhandahåller en_metod
. Om du senare refaktoriserar den första klassen, så att den inte längre har some_method
- då kommer dina tester att fortsätta att godkännas trots att din kod nu är trasig!
Mock
låter dig tillhandahålla ett objekt som en specifikation för mocken, med hjälp av nyckelordsargumentet spec. Åtkomst till metoder/attribut på mock som inte finns på ditt specifikationsobjekt kommer omedelbart att ge upphov till ett attributfel. Om du ändrar implementeringen av din specifikation kommer tester som använder den klassen att börja misslyckas omedelbart utan att du behöver instansiera klassen i dessa tester.
>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'old_method'. Did you mean: 'class_method'?
Att använda en specifikation möjliggör också en smartare matchning av anrop som görs till mock, oavsett om vissa parametrar skickades som positionella eller namngivna argument:
>>> def f(a, b, c): pass
...
>>> mock = Mock(spec=f)
>>> mock(1, 2, 3)
<Mock name='mock()' id='140161580456576'>
>>> mock.assert_called_with(a=1, b=2, c=3)
Om du vill att denna smartare matchning även ska fungera med metodanrop på mocken kan du använda auto-speccing.
Om du vill ha en starkare form av specifikation som förhindrar att godtyckliga attribut ställs in och att de hämtas, kan du använda spec_set i stället för spec.
Använda side_effect för att returnera innehåll per fil¶
mock_open()
används för att patcha open()
metoden. side_effect
kan användas för att returnera ett nytt Mock-objekt per anrop. Detta kan användas för att returnera olika innehåll per fil som lagras i en ordbok:
DEFAULT = "standard"
data_dict = {"file1": "data1",
"fil2": "data2"}
def open_side_effect(namn):
return mock_open(read_data=data_dict.get(name, DEFAULT))()
med patch("builtins.open", side_effect=open_side_effect):
med open("file1") som file1:
assert file1.read() == "data1"
med open("file2") som fil2:
assert file2.read() == "data2"
med open("file3") som fil2:
assert file2.read() == "default"
Patch Dekoratörer¶
Anteckning
Med patch()
är det viktigt att du patchar objekt i den namnrymd där de slås upp. Detta är normalt enkelt, men för en snabb guide läs where to patch.
Ett vanligt behov i tester är att patcha ett klassattribut eller ett modulattribut, t.ex. att patcha en builtin eller att patcha en klass i en modul för att testa att den instansieras. Moduler och klasser är i praktiken globala, så patchning av dem måste göras ogjord efter testet, annars kommer patchen att finnas kvar i andra tester och orsaka problem som är svåra att diagnostisera.
mock tillhandahåller tre praktiska dekoratorer för detta: patch()
, patch.object()
och patch.dict()
. patch
tar en enda sträng, av formen package.module.Class.attribute
för att ange det attribut du patchar. Det tar också valfritt ett värde som du vill att attributet (eller klassen eller vad som helst) ska ersättas med. ’patch.object’ tar ett objekt och namnet på det attribut som du vill patcha, plus eventuellt det värde som du vill patcha det med.
patch.objekt
:
>>> original = SomeClass.attribute
>>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test():
... assert SomeClass.attribute == sentinel.attribute
...
>>> test()
>>> assert SomeClass.attribute == original
>>> @patch('package.module.attribute', sentinel.attribute)
... def test():
... from package.module import attribute
... assert attribute is sentinel.attribute
...
>>> test()
Om du patchar en modul (inklusive builtins
), använd då patch()
istället för patch.object()
:
>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
... handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"
Modulnamnet kan vara ”prickat”, i formen package.module
om det behövs:
>>> @patch('package.module.ClassName.attribute', sentinel.attribute)
... def test():
... from package.module import ClassName
... assert ClassName.attribute == sentinel.attribute
...
>>> test()
Ett trevligt mönster är att faktiskt dekorera testmetoderna själva:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test_something(self):
... self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original
Om du vill patcha med en Mock kan du använda patch()
med bara ett argument (eller patch.object()
med två argument). Mocken kommer att skapas åt dig och skickas in i testfunktionen/metoden:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'static_method')
... def test_something(self, mock_method):
... SomeClass.static_method()
... mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()
Du kan stapla upp flera patchdekorationer med detta mönster:
>>> class MyTest(unittest.TestCase):
... @patch('package.module.ClassName1')
... @patch('package.module.ClassName2')
... def test_something(self, MockClass2, MockClass1):
... self.assertIs(package.module.ClassName1, MockClass1)
... self.assertIs(package.module.ClassName2, MockClass2)
...
>>> MyTest('test_something').test_something()
När du nestar patch-dekoratorer skickas mockarna in till den dekorerade funktionen i samma ordning som de tillämpas (den normala Python-ordningen som dekoratorer tillämpas). Detta innebär nerifrån och upp, så i exemplet ovan skickas mocken för test_module.ClassName2
in först.
Det finns också patch.dict()
för att ställa in värden i en ordbok bara under ett scope och återställa ordboken till dess ursprungliga tillstånd när testet avslutas:
>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
... assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original
patch
, patch.object
och patch.dict
kan alla användas som kontexthanterare.
När du använder patch()
för att skapa en mock åt dig kan du få en referens till mocken med hjälp av ”as”-formen i with-satsen:
>>> class ProductionClass:
... def method(self):
... pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
... mock_method.return_value = None
... real = ProductionClass()
... real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)
Som ett alternativ kan patch
, patch.object
och patch.dict
användas som klassdekoratorer. När de används på det här sättet är det samma sak som att tillämpa dekoratorn individuellt på varje metod vars namn börjar med ”test”.
Ytterligare exempel¶
Här är några fler exempel på lite mer avancerade scenarier.
Mocking av kedjade anrop¶
Att mocka kedjeanrop är faktiskt enkelt med mock när man väl förstår attributet return_value
. När en mock anropas för första gången, eller om du hämtar dess return_value
innan den har anropats, skapas en ny Mock
.
Detta innebär att du kan se hur objektet som returneras från ett anrop till ett mockat objekt har använts genom att fråga ut mocken return_value
:
>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)
Härifrån är det ett enkelt steg att konfigurera och sedan göra påståenden om kedjeanrop. Ett annat alternativ är förstås att skriva sin kod på ett mer testbart sätt från början…
Anta att vi har en kod som ser ut ungefär så här:
>>> class Something:
... def __init__(self):
... self.backend = BackendProvider()
... def method(self):
... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
... # more code
Om vi antar att BackendProvider
redan är väl testad, hur testar vi då method()
? Specifikt vill vi testa att kodavsnittet # more code
använder svarsobjektet på rätt sätt.
Eftersom denna kedja av anrop görs från ett instansattribut kan vi ap-patcha attributet backend
på en Something
-instans. I detta speciella fall är vi bara intresserade av returvärdet från det sista anropet till start_call
så vi har inte mycket konfiguration att göra. Låt oss anta att objektet som returneras är ”filliknande”, så vi kommer att se till att vårt svarsobjekt använder den inbyggda open()
som dess spec
.
För att göra detta skapar vi en mock-instans som vår mock-backend och skapar ett mock-svarsobjekt för det. För att ställa in svaret som returvärde för det slutliga start_call
kan vi göra så här:
mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response
Vi kan göra det på ett lite trevligare sätt genom att använda metoden configure_mock()
för att direkt ställa in returvärdet åt oss:
>>> something = Something()
>>> mock_response = Mock(spec=open)
>>> mock_backend = Mock()
>>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
>>> mock_backend.configure_mock(**config)
Med dessa kan vi sätta upp ”mock backend” på plats och göra det riktiga anropet:
>>> something.backend = mock_backend
>>> something.method()
Med hjälp av mock_calls
kan vi kontrollera det kedjade anropet med ett enda assert. Ett kedjeanrop är flera anrop i en kodrad, så det kommer att finnas flera poster i mock_calls
. Vi kan använda call.call_list()
för att skapa denna lista över anrop åt oss:
>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> assert mock_backend.mock_calls == call_list
Delvis mocking¶
I vissa tester ville jag mocka ut ett anrop till datetime.date.today()
för att returnera ett känt datum, men jag ville inte hindra koden under test från att skapa nya datumobjekt. Tyvärr är datetime.date
skriven i C, och därför kunde jag inte bara monkey-patcha ut den statiska datetime.date.today()
-metoden.
Jag hittade ett enkelt sätt att göra detta som innebar att effektivt linda in datumklassen med en mock, men passera genom anrop till konstruktören till den riktiga klassen (och returnera riktiga instanser).
Här används patch decorator
för att mocka ut klassen date
i den modul som testas. Attributet side_effect
på mock date-klassen sätts sedan till en lambda-funktion som returnerar ett riktigt datum. När mock date-klassen anropas kommer ett verkligt datum att konstrueras och returneras av side_effect
.
>>> from datetime import date
>>> with patch('mymodule.date') as mock_date:
... mock_date.today.return_value = date(2010, 10, 8)
... mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
...
... assert mymodule.date.today() == date(2010, 10, 8)
... assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)
Observera att vi inte patchar datetime.date
globalt, vi patchar date
i modulen som använder den. Se var man patchar.
När date.today()
anropas returneras ett känt datum, men anrop till date(...)
-konstruktorn returnerar fortfarande normala datum. Utan detta kan det hända att du måste beräkna ett förväntat resultat med exakt samma algoritm som den kod som testas, vilket är ett klassiskt anti-mönster för testning.
Anrop till datumkonstruktören registreras i mock_date
-attributen (call_count
och vänner), vilket också kan vara användbart för dina tester.
Ett alternativt sätt att hantera mocking dates, eller andra inbyggda klasser, diskuteras i det här blogginlägget.
Mocka en generatormetod¶
En Python-generator är en funktion eller metod som använder yield
-satsen för att returnera en serie värden när den itereras över [1].
En generatormetod/funktion anropas för att returnera generatorobjektet. Det är generatorobjektet som sedan itereras över. Protokollmetoden för iteration är __iter__()
, så vi kan mocka detta med hjälp av en MagicMock
.
Här är en exempelklass med en ”iter”-metod som implementerats som en generator:
>>> class Foo:
... def iter(self):
... for i in [1, 2, 3]:
... yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]
Hur skulle vi mocka den här klassen, och i synnerhet dess ”iter”-metod?
För att konfigurera de värden som returneras från iterationen (implicit i anropet till list
) måste vi konfigurera det objekt som returneras av anropet till foo.iter()
.
>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]
Tillämpa samma patch på alla testmetoder¶
Om du vill ha flera patchar på plats för flera testmetoder är det uppenbara sättet att tillämpa patchdekoratorerna på varje metod. Detta kan kännas som en onödig upprepning. Istället kan du använda patch()
(i alla dess olika former) som en klassdekorator. Detta applicerar patcharna på alla testmetoder i klassen. En testmetod identifieras av metoder vars namn börjar med test
:
>>> @patch('mymodule.SomeClass')
... class MyTest(unittest.TestCase):
...
... def test_one(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def test_two(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def not_a_test(self):
... return 'something'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'something'
Ett alternativt sätt att hantera patchar är att använda patchmetoder: start och stopp. Dessa gör att du kan flytta patchningen till dina metoder setUp
och tearDown
.
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... self.patcher = patch('mymodule.foo')
... self.mock_foo = self.patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
... def tearDown(self):
... self.patcher.stop()
...
>>> MyTest('test_foo').run()
Om du använder den här tekniken måste du se till att patchningen ”ångras” genom att anropa stop
. Det här kan vara krångligare än man tror, för om ett undantag uppstår i setUp så anropas inte tearDown. unittest.TestCase.addCleanup()
gör det här enklare:
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... patcher = patch('mymodule.foo')
... self.addCleanup(patcher.stop)
... self.mock_foo = patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
>>> MyTest('test_foo').run()
Spottning av obundna metoder¶
När jag skrev tester idag behövde jag patcha en obunden metod (patcha metoden på klassen snarare än på instansen). Jag behövde self som första argument eftersom jag vill göra asserts om vilka objekt som anropar just den här metoden. Problemet är att du inte kan patcha med en mock för detta, för om du ersätter en obunden metod med en mock blir den inte en bunden metod när den hämtas från instansen, och därför får den inte self passerat in. Lösningen är att patcha den obundna metoden med en riktig funktion istället. Dekoratorn patch()
gör det så enkelt att patcha ut metoder med en mock att det blir ett irritationsmoment att behöva skapa en riktig funktion.
Om du skickar autospec=True
till patch så gör den patchningen med ett verkligt funktionsobjekt. Detta funktionsobjekt har samma signatur som det som det ersätter, men delegerar till en mock under huven. Du får fortfarande din mock automatiskt skapad på exakt samma sätt som tidigare. Vad det innebär är dock att om du använder den för att lappa ut en obunden metod på en klass kommer den mockade funktionen att förvandlas till en bunden metod om den hämtas från en instans. Det kommer att ha self
passerat in som det första argumentet, vilket är precis vad jag ville ha:
>>> class Foo:
... def foo(self):
... pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
... mock_foo.return_value = 'foo'
... foo = Foo()
... foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)
Om vi inte använder autospec=True
så patchas den obundna metoden ut med en Mock-instans istället, och anropas inte med self
.
Kontroll av flera anrop med mock¶
mock har ett bra API för att göra påståenden om hur dina mock-objekt används.
>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')
Om din mock bara anropas en gång kan du använda metoden assert_called_once_with()
som också försäkrar att call_count
är ett.
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
>>> mock.foo_bar()
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
Traceback (most recent call last):
...
AssertionError: Expected 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].
Både assert_called_with
och assert_called_once_with
gör påståenden om det senaste anropet. Om din mock kommer att anropas flera gånger och du vill göra påståenden om alla dessa anrop kan du använda call_args_list
:
>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(4, 5, 6)
>>> mock()
>>> mock.call_args_list
[call(1, 2, 3), call(4, 5, 6), call()]
Med hjälpmedlet call
är det enkelt att göra påståenden om dessa anrop. Du kan bygga upp en lista över förväntade anrop och jämföra den med call_args_list
. Detta ser anmärkningsvärt likt ut som repr av call_args_list
:
>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True
Att hantera föränderliga argument¶
En annan situation är sällsynt, men kan bita dig, är när din mock anropas med mutabla argument. call_args
och call_args_list
lagrar referenser till argumenten. Om argumenten muteras av koden som testas kan du inte längre göra påståenden om vad värdena var när mocken anropades.
Här är lite exempelkod som visar problemet. Föreställ dig följande funktioner definierade i ’mymodule’:
def frob(val):
pass
def grob(val):
"Först frob och sedan clear val"
frob(val)
val.clear()
När vi försöker testa att grob
anropar frob
med rätt argument, se vad som händer:
>>> with patch('mymodule.frob') as mock_frob:
... val = {6}
... mymodule.grob(val)
...
>>> val
set()
>>> mock_frob.assert_called_with({6})
Traceback (most recent call last):
...
AssertionError: Expected: (({6},), {})
Called with: ((set(),), {})
En möjlighet skulle kunna vara att mock kopierar de argument du skickar in. Detta skulle då kunna orsaka problem om du gör påståenden som förlitar sig på objektidentitet för jämlikhet.
Här är en lösning som använder side_effect
-funktionaliteten. Om du tillhandahåller en side_effect
-funktion för en mock så kommer side_effect
att anropas med samma args som mocken. Detta ger oss en möjlighet att kopiera argumenten och lagra dem för senare påståenden. I det här exemplet använder jag en annan mock för att lagra argumenten så att jag kan använda mock-metoderna för att göra påståendet. Återigen ställer en hjälpfunktion in detta åt mig
>>> from copy import deepcopy
>>> from unittest.mock import Mock, patch, DEFAULT
>>> def copy_call_args(mock):
... new_mock = Mock()
... def side_effect(*args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... new_mock(*args, **kwargs)
... return DEFAULT
... mock.side_effect = side_effect
... return new_mock
...
>>> with patch('mymodule.frob') as mock_frob:
... new_mock = copy_call_args(mock_frob)
... val = {6}
... mymodule.grob(val)
...
>>> new_mock.assert_called_with({6})
>>> new_mock.call_args
call({6})
copy_call_args
anropas med den mock som kommer att anropas. Den returnerar en ny mock som vi gör assertionen på. Funktionen side_effect
gör en kopia av args och anropar vår new_mock
med kopian.
Anteckning
Om din mock bara ska användas en gång finns det ett enklare sätt att kontrollera argumenten vid den punkt de anropas. Du kan helt enkelt göra kontrollen inuti en side_effect
-funktion.
>>> def side_effect(arg):
... assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
...
AssertionError
Ett alternativt tillvägagångssätt är att skapa en subklass av Mock
eller MagicMock
som kopierar (med copy.deepcopy()
) argumenten. Här är ett exempel på en implementation:
>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
... def __call__(self, /, *args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... return super().__call__(*args, **kwargs)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
...
AssertionError: expected call not found.
Expected: mock({1})
Actual: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>
När du subklassar Mock
eller MagicMock
kommer alla dynamiskt skapade attribut och return_value
att använda din subklass automatiskt. Det betyder att alla barn till en CopyingMock
också kommer att ha typen CopyingMock
.
Nästande lappar¶
Att använda patch som en kontexthanterare är trevligt, men om du gör flera patchar kan du sluta med nästlade med uttalanden som indenteras längre och längre åt höger:
>>> class MyTest(unittest.TestCase):
...
... def test_foo(self):
... with patch('mymodule.Foo') as mock_foo:
... with patch('mymodule.Bar') as mock_bar:
... with patch('mymodule.Spam') as mock_spam:
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> assert mymodule.Foo is original
Med unittest cleanup
funktioner och patchmetoder: start och stopp kan vi uppnå samma effekt utan den nästlade indragningen. En enkel hjälpmetod, create_patch
, sätter patchen på plats och returnerar den skapade mocken för oss:
>>> class MyTest(unittest.TestCase):
...
... def create_patch(self, name):
... patcher = patch(name)
... thing = patcher.start()
... self.addCleanup(patcher.stop)
... return thing
...
... def test_foo(self):
... mock_foo = self.create_patch('mymodule.Foo')
... mock_bar = self.create_patch('mymodule.Bar')
... mock_spam = self.create_patch('mymodule.Spam')
...
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').run()
>>> assert mymodule.Foo is original
Mocka en ordbok med MagicMock¶
Du kanske vill låtsas vara en ordbok eller ett annat containerobjekt och registrera all åtkomst till det samtidigt som det fortfarande beter sig som en ordbok.
Vi kan göra detta med MagicMock
, som kommer att bete sig som en ordbok, och använda side_effect
för att delegera ordboksåtkomst till en verklig underliggande ordbok som är under vår kontroll.
När metoderna __getitem__()
och __setitem__()
i vår MagicMock
anropas (normal dictionary access) så anropas side_effect
med nyckeln (och i fallet __setitem__
även värdet). Vi kan också kontrollera vad som returneras.
Efter att MagicMock
har använts kan vi använda attribut som call_args_list
för att hävda hur ordlistan användes:
>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
>>> def getitem(name):
... return my_dict[name]
...
>>> def setitem(name, val):
... my_dict[name] = val
...
>>> mock = MagicMock()
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
Anteckning
Ett alternativ till att använda MagicMock
är att använda Mock
och endast tillhandahålla de magiska metoder som du specifikt vill ha:
>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)
Ett tredje alternativ är att använda MagicMock
men skicka in dict
som spec (eller spec_set) argument så att den MagicMock
som skapas endast har magiska metoder för ordböcker tillgängliga:
>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
Med dessa bieffektsfunktioner på plats kommer mock
att bete sig som en vanlig ordbok men registrera åtkomsten. Den ger till och med upphov till ett KeyError
om du försöker komma åt en nyckel som inte finns.
>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'
När den har använts kan du göra påståenden om åtkomsten med hjälp av de vanliga mock-metoderna och -attributen:
>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}
Mock-subklasser och deras attribut¶
Det finns olika anledningar till varför du kanske vill underklassa Mock
. En anledning kan vara att lägga till hjälpmetoder. Här är ett fånigt exempel:
>>> class MyMock(MagicMock):
... def has_been_called(self):
... return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True
Standardbeteendet för Mock
-instanser är att attribut och returvärden är av samma typ som den mock de är åtkomliga på. Detta säkerställer att Mock
-attribut är Mocks
och MagicMock
-attribut är MagicMocks
[2]. Så om du subklassar för att lägga till hjälpmetoder så kommer de också att vara tillgängliga på attributen och returvärdet mock av instanser av din subklass.
>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True
Ibland är detta obekvämt. Till exempel, en användare subklassar mock för att skapa en Twisted adapter. Att ha detta tillämpat på attribut också orsakar faktiskt fel.
Mock
(i alla dess varianter) använder en metod som heter _get_child_mock
för att skapa dessa ”sub-mocks” för attribut och returvärden. Du kan förhindra att din subklass används för attribut genom att åsidosätta den här metoden. Signaturen är att den tar godtyckliga nyckelordsargument (**kwargs
) som sedan skickas till mock-konstruktören:
>>> class Subclass(MagicMock):
... def _get_child_mock(self, /, **kwargs):
... return MagicMock(**kwargs)
...
>>> mymock = Subclass()
>>> mymock.foo
<MagicMock name='mock.foo' id='...'>
>>> assert isinstance(mymock, Subclass)
>>> assert not isinstance(mymock.foo, Subclass)
>>> assert not isinstance(mymock(), Subclass)
Ett undantag från denna regel är de icke-kallbara mockarna. Attributen använder den anropsbara varianten eftersom icke anropsbara mocks annars inte skulle kunna ha anropsbara metoder.
Mocking-import med patch.dict¶
En situation där mocking kan vara svårt är när du har en lokal import inuti en funktion. Dessa är svårare att mocka eftersom de inte använder ett objekt från modulens namnrymd som vi kan patcha ut.
I allmänhet bör lokal import undvikas. De görs ibland för att förhindra cirkulära beroenden, för vilka det vanligtvis finns ett mycket bättre sätt att lösa problemet (refaktorisera koden) eller för att förhindra ”up front-kostnader” genom att fördröja importen. Detta kan också lösas på bättre sätt än en ovillkorlig lokal import (lagra modulen som ett klass- eller modulattribut och gör bara importen vid första användningen).
Bortsett från det finns det ett sätt att använda mock
för att påverka resultatet av en import. Import hämtar ett objekt från ordlistan sys.modules
. Observera att det hämtar ett objekt, som inte behöver vara en modul. Om du importerar en modul för första gången resulterar det i att ett modulobjekt läggs in i sys.modules
, så när du importerar något får du vanligtvis en modul tillbaka. Detta behöver dock inte vara fallet.
Detta innebär att du kan använda patch.dict()
för att tillfälligt sätta en mock på plats i sys.modules
. Alla importer medan den här patchen är aktiv kommer att hämta mocken. När patchen är klar (den dekorerade funktionen avslutas, with-satsens kropp är klar eller patcher.stop()
anropas) kommer det som fanns där tidigare att återställas på ett säkert sätt.
Här är ett exempel som låtsas vara modulen ”fooble”.
>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... import fooble
... fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()
Som du kan se lyckas import fooble
, men vid avslut finns det ingen fooble kvar i sys.modules
.
Detta fungerar också för formuläret från modulens importnamn
:
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... from fooble import blob
... blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()
Med lite mer arbete kan du också mocka paketimport:
>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
... from package.module import fooble
... fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()
Spårning av anropsordning och mindre verbala anropsutlåtanden¶
Klassen Mock
låter dig spåra ordningen på metodanropen på dina mock-objekt genom attributet method_calls
. Detta tillåter dig inte att spåra ordningen på anrop mellan separata mock-objekt, men vi kan använda mock_calls
för att uppnå samma effekt.
Eftersom attrapper spårar anrop till barnattrapper i mock_calls
, och åtkomst till ett godtyckligt attribut hos en attrapp skapar en barnattrapp, kan vi skapa våra separata attrapper från en förälder. Anrop till dessa barn mock kommer då alla att registreras, i ordning, i mock_calls
av föräldern:
>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]
Vi kan sedan hävda anropen, inklusive ordningen, genom att jämföra med attributet mock_calls
på manager mock:
>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True
Om patch
skapar, och sätter på plats, dina mockar så kan du koppla dem till en manager mock med hjälp av attach_mock()
metoden. Efter att ha bifogat anrop kommer att spelas in i mock_calls
av chefen.
>>> manager = MagicMock()
>>> with patch('mymodule.Class1') as MockClass1:
... with patch('mymodule.Class2') as MockClass2:
... manager.attach_mock(MockClass1, 'MockClass1')
... manager.attach_mock(MockClass2, 'MockClass2')
... MockClass1().foo()
... MockClass2().bar()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[call.MockClass1(),
call.MockClass1().foo(),
call.MockClass2(),
call.MockClass2().bar()]
Om många anrop har gjorts, men du bara är intresserad av en viss sekvens av dem, är ett alternativ att använda metoden assert_has_calls()
. Denna tar en lista med anrop (konstruerad med call
-objektet). Om den sekvensen av anrop finns i mock_calls
så lyckas assert.
>>> m = MagicMock()
>>> m().foo().bar().baz()
<MagicMock name='mock().foo().bar().baz()' id='...'>
>>> m.one().two().three()
<MagicMock name='mock.one().two().three()' id='...'>
>>> calls = call.one().two().three().call_list()
>>> m.assert_has_calls(calls)
Trots att de kedjade anropen m.one().two().three()
inte är de enda anrop som har gjorts till mock, lyckas ändå assert.
Ibland kan en mock ha flera anrop till sig, och du är bara intresserad av att försäkra dig om vissa av dessa anrop. Du kanske inte ens bryr dig om ordningen. I det här fallet kan du skicka any_order=True
till assert_has_calls
:
>>> m = MagicMock()
>>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
(...)
>>> calls = [call.fifty('50'), call(1), call.seven(7)]
>>> m.assert_has_calls(calls, any_order=True)
Mer komplex argumentmatchning¶
Genom att använda samma grundläggande koncept som ANY
kan vi implementera matchare för att göra mer komplexa påståenden om objekt som används som argument till mocks.
Anta att vi förväntar oss att ett objekt ska skickas till en mock som som standard jämför lika baserat på objektidentitet (vilket är Python-standarden för användardefinierade klasser). För att använda assert_called_with()
skulle vi behöva skicka in exakt samma objekt. Om vi bara är intresserade av några av attributen för detta objekt kan vi skapa en matchare som kontrollerar dessa attribut åt oss.
I det här exemplet kan du se hur ett ”vanligt” anrop till assert_called_with
inte är tillräckligt:
>>> class Foo:
... def __init__(self, a, b):
... self.a, self.b = a, b
...
>>> mock = Mock(return_value=None)
>>> mock(Foo(1, 2))
>>> mock.assert_called_with(Foo(1, 2))
Traceback (most recent call last):
...
AssertionError: expected call not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__main__.Foo object at 0x...>)
En jämförelsefunktion för vår klass Foo
kan se ut ungefär så här:
>>> def compare(self, other):
... if not type(self) == type(other):
... return False
... if self.a != other.a:
... return False
... if self.b != other.b:
... return False
... return True
...
Och ett matchningsobjekt som kan använda jämförelsefunktioner som denna för sin jämlikhetsoperation skulle se ut ungefär så här:
>>> class Matcher:
... def __init__(self, compare, some_obj):
... self.compare = compare
... self.some_obj = some_obj
... def __eq__(self, other):
... return self.compare(self.some_obj, other)
...
Om vi lägger ihop allt detta:
>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)
Matcher
instansieras med vår jämförelsefunktion och Foo
-objektet som vi vill jämföra mot. I assert_called_with
kommer Matcher
equality-metoden att anropas, vilket jämför objektet som mocken anropades med mot det som vi skapade vår matcher med. Om de matchar så går assert_called_with
igenom, och om de inte gör det så uppstår ett AssertionError
:
>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})
Med lite finjusteringar kan du få jämförelsefunktionen att lyfta AssertionError
direkt och ge ett mer användbart felmeddelande.
Från och med version 1.5 tillhandahåller Python-testbiblioteket PyHamcrest liknande funktionalitet, som kan vara användbar här, i form av dess jämlikhetsmatchning (hamcrest.library.integration.match_equality).