Een inleiding tot Mocking in Python

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 voor het uitwerpen van een CD heeft genoemd, in tegenstelling tot het feit dat uw cd-lade elke keer als een test wordt uitgevoerd geopend is.,

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 mockaan 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:

  1. Mock out deRemovalService.rm methode zelf.,
  2. 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.,

Share

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *