hoe Unit Tests in Python uit te voeren zonder je geduld te testen
vaak werkt de software die we schrijven direct samen met wat we als “vuile” diensten zouden bestempelen. In lekentaal: diensten die cruciaal zijn voor onze toepassing, maar waarvan de interacties ongewenste neveneffecten hebben bedoeld, dat wil zeggen ongewenst in het kader van een autonome test.,Facebook Facebook bijvoorbeeld: misschien schrijven we een sociale app en willen we onze nieuwe ‘Post to Facebook feature’ testen, maar we willen niet elke keer dat we onze test suite uitvoeren op Facebook posten.
De Python unittest
bibliotheek bevat een subpakket met de naam unittest.mock
—of als u het als een afhankelijkheid verklaart, gewoon mock
—die uiterst krachtige en nuttige middelen biedt om deze ongewenste bijwerkingen te bespotten en uit te schakelen.,
opmerking: mock
is nieuw opgenomen in de standaardbibliotheek vanaf Python 3.3; eerdere distributies moeten de Mockbibliotheek gebruiken die via PyPI kan worden gedownload.
systeemaanroepen vs. Python Spot
om je een ander voorbeeld te geven, en een waarmee we de rest van het artikel zullen draaien, overweeg systeemaanroepen., Het is niet moeilijk om te zien dat dit uitstekende kandidaten zijn voor spot: of u nu een script schrijft om een CD-station uit te werpen, een webserver die verouderde cache-bestanden verwijdert van /tmp
, of een socket-server die bindt aan een TCP-poort, deze aanroepen hebben allemaal ongewenste bijwerkingen in de context van uw unit-tests.
als ontwikkelaar geeft u er meer om dat uw bibliotheek met succes de systeemfunctie heeft aangeroepen voor het uitwerpen van een CD (met de juiste argumenten, enz.) in tegenstelling tot het daadwerkelijk ervaren van uw CD lade open elke keer dat een test wordt uitgevoerd. (Of erger nog, meerdere keren, als meerdere tests verwijzen naar de eject code tijdens een enkele unit – test run!)
Op dezelfde manier betekent het efficiënt en performant houden van uw unit-tests dat u zoveel “langzame code” buiten de geautomatiseerde test runs houdt, namelijk bestandssysteem en netwerktoegang.,
in ons eerste voorbeeld zullen we een standaard Python-testcase refactor van de originele vorm naar een met mock
. We laten zien hoe het schrijven van een testcase met spotters onze tests slimmer, sneller en in staat om meer te onthullen over hoe de software werkt.
een eenvoudige Delete functie
We moeten allemaal van tijd tot tijd bestanden van ons bestandssysteem verwijderen, Dus laten we een functie in Python schrijven die het voor onze scripts een beetje gemakkelijker zal maken om dit te doen.,
#!/usr/bin/env python# -*- coding: utf-8 -*-import osdef rm(filename): os.remove(filename)
uiteraard biedt onze rm
methode op dit moment niet veel meer dan de onderliggende os.remove
methode, maar onze codebase zal verbeteren, waardoor we hier meer functionaliteit kunnen toevoegen.
laten we een traditionele testcase schrijven, d.w.z. zonder mocks:
onze testcase is vrij eenvoudig, maar elke keer dat het wordt uitgevoerd, wordt een tijdelijk bestand aangemaakt en vervolgens verwijderd., Bovendien kunnen we niet testen of onze rm
methode het argument correct doorgeeft aan de os.remove
aanroep. We kunnen aannemen dat dit op basis van de bovenstaande test wel het geval is, maar er is nog veel te wensen over.
Refactoring met Python Mocks
laten we onze testcase refactor metmock
:
Met deze refactors hebben we de manier waarop de test werkt fundamenteel veranderd. We hebben een insider, een object dat we kunnen gebruiken om de functionaliteit van een ander te verifiëren.,
Potential Python Mocking valkuilen
een van de eerste dingen die moeten opvallen is dat we de mock.patch
method decorator gebruiken om een object te bespotten dat zich op mymodule.os
bevindt, en die mock in onze test case methode injecteren. Zou het niet zinvoller zijn om gewoon os
zelf te bespotten, in plaats van de verwijzing ernaar op mymodule.os
?
nou, Python is een beetje een stiekeme slang als het gaat om het importeren en beheren van modules., Tijdens runtime heeft demymodule
module zijn eigenos
die wordt geïmporteerd in zijn eigen lokale scope in de module. Dus als we os
mock, zullen we de effecten van de mock niet zien in de mymodule
module.
De mantra om te blijven herhalen is dit:
Mock een item waar het wordt gebruikt, niet waar het vandaan komt.,
Als u de mock tempfile
module voor myproject.app.MyElaborateClass
moet uitvoeren, moet u waarschijnlijk de mock toepassen op myproject.app.tempfile
, aangezien elke module zijn eigen import behoudt.
met die valkuil uit de weg, laten we de spot blijven drijven.
validatie toevoegen aan ‘rm’
derm
methode die eerder is gedefinieerd, is nogal simplistisch. We willen graag dat het valideren dat een pad bestaat en is een bestand voordat gewoon blindelings proberen om het te verwijderen., Laten we refactor rm
om een beetje slimmer te zijn:
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport os.pathdef rm(filename): if os.path.isfile(filename): os.remove(filename)
geweldig. Nu, laten we onze test case aanpassen om de dekking te houden.
ons testparadigma is volledig veranderd. We kunnen nu interne functionaliteit van methoden verifiëren en valideren zonder bijwerkingen.
File-Removal as a Service with Mock Patch
tot nu toe hebben we alleen gewerkt met het leveren van mocks voor functies, maar niet voor methoden op objecten of gevallen waar mocking nodig is voor het verzenden van parameters. Laten we eerst objectmethoden behandelen.,
we beginnen met een refactor van de rm
methode in een service Klasse. Er is op zich geen gerechtvaardigde noodzaak om zo ‘ n eenvoudige functie in een object te kapselen, maar het zal ons op zijn minst helpen om sleutelconcepten in mock
aan te tonen. Let ‘ s refactor:
u zult merken dat er niet veel veranderd is in onze testcase:
geweldig, dus we weten nu dat de RemovalService
werkt zoals gepland., Laten we een andere service maken die het als een afhankelijkheid verklaart:
aangezien we al testdekking hebben op de RemovalService
, gaan we de interne functionaliteit van de rm
methode niet valideren in onze tests van UploadService
. In plaats daarvan zullen we gewoon testen (zonder bijwerkingen natuurlijk) dat UploadService
de RemovalService.rm
methode aanroept, die we “just works™” kennen van onze vorige testcase.
Er zijn twee manieren om dit te doen:
- Mock out de
RemovalService.rm
methode zelf., - lever een bespot exemplaar in de constructor van
UploadService
.
omdat beide methoden vaak belangrijk zijn in unit-testing, zullen we beide bekijken.
Optie 1: Mocking Instance Methods
De mock
bibliotheek heeft een speciale method decorator voor mocking object instance methods en properties, de @mock.patch.object
decorator:
geweldig! We hebben gevalideerd dat de UploadService
succesvol de rm
methode van onze instantie aanroept. Valt je iets interessants op?, Het patching mechanisme verving eigenlijk derm
methode van alleRemovalService
instanties in onze testmethode. Dat betekent dat we de instanties zelf kunnen inspecteren. Als je meer wilt zien, probeer dan een breekpunt in je spotcode te laten vallen om een goed gevoel te krijgen voor hoe het patching mechanisme werkt.
Mock Patch valkuil: Decorator volgorde
bij het gebruik van meerdere decorateurs op uw testmethoden, volgorde is belangrijk, en het is een beetje verwarrend. Kortom, bij het in kaart brengen van decorateurs om de methode parameters, werken achteruit., Beschouw dit voorbeeld:
merk op hoe onze parameters worden afgestemd op de omgekeerde volgorde van de decorateurs? Dat komt deels door de manier waarop Python werkt. Met meerdere methodedecorators, hier is de volgorde van uitvoering in pseudocode:
patch_sys(patch_os(patch_os_path(test_something)))
aangezien de patch naar sys
de buitenste patch is, zal het als laatste worden uitgevoerd, waardoor het de laatste parameter is in de werkelijke testmethode argumenten. Let op dit goed en gebruik een debugger bij het uitvoeren van uw tests om ervoor te zorgen dat de juiste parameters worden geïnjecteerd in de juiste volgorde.,
Optie 2: Mock Instances aanmaken
in plaats van de specifieke instance methode te bespotten, kunnen we in plaats daarvan een mocked instance leveren aan UploadService
met de constructor. Ik geef de voorkeur aan Optie 1 hierboven, omdat deze veel preciezer is, maar er zijn veel gevallen waarin optie 2 efficiënt of noodzakelijk kan zijn. Laten we onze test opnieuw refactor:
in dit voorbeeld hebben we zelfs geen functionaliteit hoeven patchen, we maken gewoon een auto-spec voor de RemovalService
klasse, en injecteren deze instantie in onze UploadService
om de functionaliteit te valideren.,
de mock.create_autospec
methode maakt een functioneel equivalent aan de opgegeven klasse. Wat dit betekent, praktisch gesproken, is dat wanneer de geretourneerde instantie wordt interactie met, het zal leiden tot uitzonderingen als gebruikt in illegale manieren. Meer specifiek, als een methode wordt aangeroepen met het verkeerde aantal argumenten, zal een uitzondering worden gemaakt. Dit is uiterst belangrijk als refactors gebeuren. Als een bibliotheek verandert, breken tests en dat wordt verwacht. Zonder een auto-spec te gebruiken, slagen onze tests nog steeds, ook al is de onderliggende implementatie verbroken.,
valkuil: de mock.Mock
en mock.MagicMock
klassen
De mock
bibliotheek bevat ook twee belangrijke klassen waarop de meeste interne functionaliteit is gebouwd: mock.Mock
en mock.MagicMock
. Als u de keuze hebt om een mock.Mock
instantie te gebruiken, een mock.MagicMock
instantie, of een auto-spec, geef dan altijd de voorkeur aan het gebruik van een auto-spec, omdat het uw tests gezond houdt voor toekomstige wijzigingen., Dit komt omdat mock.Mock
en mock.MagicMock
alle methode aanroepen en eigendomstoewijzingen accepteren, ongeacht de onderliggende API. Overweeg het volgende use case:
class Target(object): def apply(value): return valuedef method(target, value): return target.apply(value)
We kunnen dit testen met een mock.Mock
instantie als volgt:
deze logica lijkt normaal, maar laten we de Target.apply
methode aanpassen om meer parameters te nemen:
class Target(object): def apply(value, are_you_sure): if are_you_sure: return value else: return None
voer uw test opnieuw uit en u zult zien dat het nog steeds doorgaat. Dat komt omdat het niet is gebouwd tegen uw werkelijke API., Daarom moet u altijd de create_autospec
methode en de autospec
parameter gebruiken met de @patch
en @patch.object
decorators.Facebook Facebook Mock voorbeeld: het bespotten van een Facebook API Call
om af te ronden, laten we een meer toepasbaar real-world python mock voorbeeld schrijven, een die we in de inleiding vermeldden: het plaatsen van een bericht op Facebook. We schrijven een mooie wikkelles en een bijbehorende testcase.,
Hier is onze testcase, die controleert of we het bericht plaatsen zonder het bericht daadwerkelijk te plaatsen:
zoals we tot nu toe hebben gezien, is het heel eenvoudig om slimmere tests te schrijven met mock
in Python.
conclusie
Python ’s mock
bibliotheek, als een beetje verwarrend om mee te werken, is een game-changer voor unit-testing. We hebben veelvoorkomende use-cases aangetoond voor het starten met mock
In unit-testing, en hopelijk zal dit artikel Python ontwikkelaars helpen om de eerste hindernissen te overwinnen en uitstekende, geteste code te schrijven.,