det är bara ett faktum av livet, eftersom koden växer så småningom måste du börja lägga till mocks till din testsvit. Det som började som ett gulligt litet tvåklassprojekt pratar nu med externa tjänster och du kan inte testa det bekvämt längre.

det är därför Python levereras medunittest.mock, en kraftfull del av standardbiblioteket för Stubb beroenden och gäckande biverkningar.

unittest.mock är dock inte särskilt intuitivt.,

Jag har funnit mig själv många gånger undrar varför min go-to recept inte fungerar för ett visst fall, så jag har satt ihop detta cheatsheet för att hjälpa mig själv och andra få hånar arbetar snabbt.

Du hittar kodexemplen i artikelns GitHub-arkiv. Jag kommer att använda Python 3.6, om du använder 3.2 eller under måste du använda mock PyPI-paketet.,

exemplen skrivs medunittest.TestCase klasser för enkelhet i att utföra dem utan beroenden, men du kan skriva dem som funktioner med pytest nästan direkt,unittest.mock fungerar bra. Om du är en pytest användare men jag uppmuntrar dig att ta en titt på det utmärkta pytest-mock biblioteket.

centerpunkten förunittest.mock – modulen är naturligtvisMock – klassen., Det viktigaste kännetecknet för en Mock objekt är att det kommer att returnera en annan Mock instans när:

  • åtkomst till en av dess attribut
  • anropa själva objektet

detta är standardbeteendet, men det kan åsidosättas på olika sätt. Till exempel kan du tilldela ett värde till ett attribut i Mock av:

  • tilldela det direkt, som du skulle göra med något Python-objekt.,
  • användconfigure_mock – metoden i en instans.
  • eller skicka sökordsargument till klassenMock vid skapande.

för att åsidosätta samtal till mock måste du konfigurera dessreturn_value egendom, även tillgänglig som sökordsargument iMock initialiserare., Mock kommer alltid att returnera samma värde för alla samtal.detta kan också konfigureras med attributet side_effect:

  • Om du vill returnera olika värden för varje samtal kan du tilldela side_effect.
  • Om du vill höja ett undantag när du anropar Mock kan du helt enkelt tilldela undantagsobjektet till side_effect.,

med alla dessa verktyg kan vi nu skapa stubbar för i huvudsak alla Python-objekt, vilket kommer att fungera bra för ingångar till vårt system. Men vad sägs om utgångar?

om du gör ett CLI-program för att ladda ner hela Internet, Vill du förmodligen inte ladda ner hela Internet på varje test. Istället skulle det räcka att hävda att requests.download_internet (inte en riktig metod) kallades på lämpligt sätt. Mock ger dig praktiska metoder för att göra det.,

notera i vårt exempelassert_called_once misslyckades, detta visar en annan viktig aspekt avMock objekt, de registrerar alla interaktioner med dem och du kan sedan inspektera dessa interaktioner.

Du kan till exempel använda call_count för att hämta antalet samtal till Mock och använda call_args eller call_args_list för att inspektera argumenten till de sista respektive alla samtal.,

om detta är obekvämt när som helst kan du använda metodenreset_mock för att rensa de inspelade interaktionerna, notera att konfigurationen inte återställs, bara interaktionerna.

låt mig slutligen introduceraMagicMock, en underklass avMock som implementerar standard magic eller dundermetoder. Detta gör MagicMock idealisk för att håna klassbeteende, varför det är standardklassen när du lappar.,

Vi är nu redo att börja håna och isolera enheten under tester. Här är några recept att komma ihåg:

Patch vid import

det viktigaste sättet att användaunittest.mock är att lappa import i modulen under test med hjälp avpatch – funktionen.

patch kommer att avlyssna import uttalanden som identifierats av en sträng (mer om det senare) och returnera en Mock – instans du kan förkonfigurera med hjälp av de tekniker vi diskuterade ovan.,

Tänk att vi vill testa denna mycket enkla funktion:

Observera att vi importerar os och ringer getcwd för att få den aktuella arbetskatalogen. Vi vill inte faktiskt kalla det på våra tester men eftersom det inte är viktigt att vår kod och returvärdet kan skilja sig mellan miljöer det körs på.

som nämnts ovan måste vi leverera patch med en sträng som representerar vår specifika import., Vi vill inte leverera helt enkeltos.getcwd eftersom det skulle lappa det för alla moduler, istället vill vi leverera modulen under testimport avos , dvswork.os . När modulen importeraspatch kommer att arbeta sin magi och returnera enMock istället.,

Alternativt kan vi använda dekoratörsversionen avpatch, notera den här gången testet har en extra parameter:mocked_os till vilkenMock injiceras i testet.,

patch kommer att vidarebefordra sökordsargument till klassenMock, så att konfigurera enreturn_value vi lägger helt enkelt till den som en:

mocking klasser

det är ganska vanligt att Patch klasser helt eller delvis., Helper med "db"

  • Worker returnerar den förväntade sökvägen som Helper
  • för att testa Worker I fullständig isolering måste vi lappa hela

    klass:

    notera dubbel return_value I exemplet, helt enkelt använda MockHelper.get_path.return_value skulle inte fungera eftersom det i koden vi kallar get_path I en instans, inte själva klassen.,

    kedjningssyntaxen är lite förvirrande men kom ihåg MagicMockreturnerar en annanMagicMockpå samtal__init__. Här konfigurerar vi alla falskaHelper instanser som skapats avMockHelper för att returnera vad vi förväntar oss på samtal tillget_path vilket är den enda metoden vi bryr oss om i vårt test.,

    Class speccing

    en följd av flexibiliteten hosMock är att när vi har hånat en klass Python inte kommer att höjaAttributeError eftersom det helt enkelt kommer att returnera nya instanser avMagicMock för i princip allt. Detta är oftast en bra sak men kan leda till vissa förvirrande beteende och potentiellt buggar. Till exempel skriva följande test,

    tyst passerar utan varning helt saknas stavfel i assrt_called_once .,

    dessutom, om vi skulle byta namn på Helper.get_path till Helper.get_folder, men glöm att uppdatera samtalet i Worker våra tester kommer fortfarande att passera:

    lyckligtvis Mock levereras med ett verktyg för att förhindra dessa fel, speccing.

    enkelt uttryckt, det förkonfigurerar hånar att bara svara på metoder som faktiskt finns i spec-klassen., Det finns flera sätt att definiera specifikationer, men det enklaste är att helt enkelt skickaautospec=True tillpatch – Anropet, vilket kommer att konfigureraMock att bete sig som objektet som hånas, vilket ökar undantagen för saknade attribut och felaktiga signaturer efter behov., Till exempel:

    partiell klass mocking

    om du är mindre benägen att testa i fullständig isolering kan du också delvis lappa en klass med patch.object:

    här patch.object tillåter oss att konfigurera en hånad version av get_path endast, vilket lämnar resten av beteendet orört., Naturligtvis innebär detta att testet inte längre är vad du strikt skulle överväga en enhet test men du kan vara ok med det.

    Mocking inbyggda funktioner och miljövariabler

    i de tidigare exemplen försummade vi att testa en viss aspekt av vår enkla klass, Anropetprint. Om det i samband med din ansökan är viktigt, som ett CLI-kommando till exempel, måste vi göra påståenden mot det här samtalet.,

    print är naturligtvis en inbyggd funktion i Python, vilket innebär att vi inte behöver importera den i vår modul, vilket strider mot vad vi diskuterade ovan om patchning vid import., Föreställ dig fortfarande att vi hade den här lite mer komplicerade versionen av vår funktion:

    Vi kan skriva ett test som följande:

    notera några saker:

    1. vi kan håna print utan problem och hävda att det fanns ett samtal genom att följa ”patch på import” – regeln. Detta var dock en förändring som infördes i 3.,5, tidigare behövde du lägga till create=True till patch Ring för att signalera unittest.mockför att skapa en Mock även om ingen import matchar identifieraren.
    2. vi använderpatch.dict för att injicera en temporär miljövariabel ios.environ, detta kan utökas till någon annan ordlista som vi vill håna.,
    3. vi häckar flerapatch context manager samtal men bara använder as I den första eftersom det är den vi behöver använda för att ringa assert_called_once_with .,

    om du inte är förtjust i att häcka kontextchefer kan du också skriva patch samtal i dekoratörsformen:

    notera dock ordningen på argumenten till testet matchar staplingsordningen för dekoratörer, och även om du inte gillar att häcka kontextchefer.att patch.dict inte injicerar ett argument.,detta mycket vanliga scenario:

    Du kan naturligtvis lägga till en faktisk fixturfil, men i verkliga fall kan det inte vara ett alternativ, istället kan vi håna kontexthanterarens utdata för att vara ett StringIO objekt:

    det finns inget speciellt här förutom den magiska __enter__ – metoden, vi måste bara komma ihåg den underliggande mekaniken för kontexthanterare och en smart användning av vår pålitliga MagicMock .,

    Mocking class attributes

    det finns många sätt att uppnå detta men vissa är mer idiotsäkra att andra. Säg att du har skrivit följande kod:

    Du kan testa koden utan några mockar på två sätt:

    1. Om koden under test öppnar attributet via self.ATTRIBUTE, vilket är fallet i det här exemplet, kan du helt enkelt ställa in attributet direkt i instansen. Detta är ganska säkert eftersom förändringen är begränsad till denna enda instans.,
    2. Alternativt kan du även ange attributet i den importerade klassen i testet innan du skapar en instans. Detta ändrar dock klassattributet du har importerat i testet vilket kan påverka följande tester, så du måste komma ihåg att återställa det.

    den största nackdelen med denna icke-Mock tillvägagångssätt är att om du vid något tillfälle byta namn på attributet dina tester kommer att misslyckas och felet kommer inte direkt påpeka denna namngivning obalans.,

    för att lösa att vi kan använda patch.object på den importerade klassen som kommer att klaga om klassen inte har det angivna attributet.

    här är några tester som använder varje teknik:

    Mocking class helpers

    följande exempel är roten till många problem med monkeypatching med Mock. Det visar vanligtvis på mer mogna kodbaser som börjar använda ramar och hjälpare vid klassdefinitionen., Föreställ dig till exempel denna hypotetiska Field class helper:

    syftet är att slå in och förbättra ett attribut i en annan klass, ett ganska mönster som du vanligtvis kan se i ORMs eller formulärbibliotek. Oroa dig inte för mycket med detaljerna i det bara notera att det finns en type och default värde passerade in.,

    ta nu detta andra prov av produktionskod:

    den här klassen använder klassenField genom att definiera desscountry attribut som en vars typ ärField”51f3a9f0e5″>

    och en default som det första elementet i konstanten COUNTRIES. Logiken under test är att beroende på landet tillämpas en rabatt.,

    för vilket vi kan skriva följande test:

    men det skulle inte passera.,

    låt oss gå igenom testet:

    1. först patchar standardländerna i pricer för att vara en lista med en enda post GB ,
    2. detta bör göra CountryPricer.country attributet standard till den posten eftersom det är definitionen inkluderar default=COUNTRIES
  • det sedan instanciates CountryPricer och ber om rabatterat pris för GB!
  • så vad händer?,

    orsaken till detta ligger i Pythons beteende under importen, som bäst beskrivs i Luciano Ramalhos utmärkta flytande Python på kapitel 21:

    för klasser är historien annorlunda: vid importtid utför tolken kroppen i varje klass, även kroppen av klasser kapslade i andra klasser. Utförande av en klasskropp innebär att klassens attribut och metoder definieras, och sedan byggs klassobjektet självt.,

    tillämpa detta på vårt exempel:

    1. country attributetsField – instans är byggd innan testet körs vid importtid,
    2. Python läser klassens kropp och passerarCOUNTRIESCOUNTRIESField – instansen,
    3. vår testkod körs men det är för sent för oss att lappa COUNTRIES och få ett korrekt påstående.,

    från ovanstående beskrivning kan du försöka fördröja importen av modulen tills inuti testen, något som:

    detta kommer dock fortfarande inte att passera som mock.patch importerar modulen och sedan monkeypatch den, resulterar i samma situation som tidigare.,

    för att arbeta runt detta måste vi omfamna tillståndet för CountryPricer – klassen vid tidpunkten för testet och patchen default på den redan initierade Field – instansen:

    den här lösningen är inte idealisk eftersom den kräver kunskap om de interna delarna av Field som du kanske inte har eller vill använda om du använder ett externt bibliotek men det fungerar på det här förenklade fallet.,

    problemet med importtiden är ett av de största problemen jag har stött på när jag användeunittest.mock. Du måste komma ihåg när du använder den att vid importtidskoden på toppnivå av moduler exekveras, inklusive klasskroppar.

    om logiken du testar är beroende av någon av denna logik kan du behöva tänka om hur du använder patch I enlighet därmed.,

    inslagning

    denna introduktion och cheatsheet ska få dig att håna med unittest.mock, men jag uppmuntrar dig att läsa dokumentationen noggrant, det finns gott om mer intressanta knep att lära.

    det är värt att nämna att det finns alternativ tillunittest.mock, särskilt Alex Gaynorspretend bibliotek i kombination medpytest’smonkeypatch fixtur. Som Alex påpekar, med hjälp av dessa två bibliotek kan du ta ett annat tillvägagångssätt för att göra mocking strängare ännu mer förutsägbar., Definitivt ett tillvägagångssätt värt att utforska men utanför ramen för denna artikel.

    unittest.mock är för närvarande standard för mocking i Python och du hittar den i nästan alla kodbaser. Att ha en solid förståelse för det hjälper dig att skriva bättre tester snabbare.

    Articles