Pytest's Assert is Niet Wat Je Denkt Dat Het Is

Wat is AST? En hoe hackt pytest het om je een betere UX te geven?

In Python, zoals in veel andere talen, is er een statement dat een gegeven conditie controleert, en een AssertionError verhoogt als deze conditie False is, anders doet het niets als de gegeven conditie True is. Dat is in principe wat assert doet.

Python's assert statement

Dit maakt assert een goede kandidaat voor unit tests. Uiteindelijk test je een bepaalde conditie, en wil je dat je test faalt als die conditie faalt.

Desalniettemin is er hier één klein probleem. De AssertionError geeft je minimale informatie. Bekijk de volgende code; het vertelt je dat de twee lijsten niet gelijk zijn, maar vertelt niet echt welke elementen van de twee lijsten verschillen:

Assert errors zijn niet erg beschrijvend

Vergelijk dat met Pytest's output, wanneer hetzelfde assert statement wordt gebruikt:

Meer beschrijvende foutmeldingen, wanneer pytest betrokken is

Deze output is zeker nuttiger, maar hoe zorgt pytest ervoor dat zijn asserts zich anders gedragen?!

Kunnen we een keyword in Python overschrijven?

Deze vraag stuurde me in een rabbit hole, en hielp me begrijpen hoe pytest een beetje beter werkt.

De enige manier voor pytest om zo'n uitgebreide output te bieden is om het assert keyword in Python te overschrijven en zijn gedrag te veranderen. Na een snelle zoekopdracht zul je echter vinden dat er geen manier is om een van de taal keywords te overschrijven.

Andere bibliotheken of modules weten dat. Dus unittest creëert bijvoorbeeld zijn eigen methoden in plaats van het ingebouwde assert keyword te gebruiken. Bekijk de assertEqual methode van de module hieronder:

Unittest heeft methoden die het assert statement nabootsen

Dus, hoe komt pytest weg met het toch gebruiken van assert? Om deze vraag te beantwoorden, moeten we leren over Abstract Syntax Trees, of AST voor kort.

Abstract Syntax Tree (AST)

Wanneer je een stuk code uitvoert, parseert Python het eerst in tokens: "if", "for", "x", "3.14", "==", enz. Deze tokens zijn georganiseerd in wat bekend staat als Abstract Syntax Tree (AST). Deze boom is wat bijhoudt welke delen van de code bij elkaar horen, welke delen vóór de anderen worden uitgevoerd, enzovoort. Deze boom is wat ervoor zorgt dat het resultaat van de eerste vergelijking hieronder 7 is, terwijl dat van de tweede vergelijking 9 is:

AST vergelijkingen voorbeeld

In essentie is de AST hoe tokens worden georganiseerd volgens de grammatica van de programmeertaal. En later wordt deze boom gecompileerd naar byte code, voor de Python interpreter om het uit te voeren.

Één cool ding over Python, is dat het je toestaat om met deze boom te spelen. Wanneer je een module in je code importeert, krijg je hooks naar de machinerie van dat importproces. Daar kun je doen wat je wilt. Je kunt delen van de code bewerken voordat je het uitvoert, de abstract syntax tree on the fly veranderen; de lucht is je limiet.

Dat is in principe wat pytest doet. Wanneer het je unit test bestanden laadt, parseert het hun AST's, en verandert ze om elk assert statement te vervangen door een if conditie. Hier is een deel van pytest's broncode, uitleg van wat het doet in de docstring van de methode:

Pytest's broncode

Gelukkig, aangezien Python de code toch moet converteren naar tokens en ze in AST's moet zetten, komt de taal met een ingebouwde module voor het verwerken van die grammaticabomen, dus noch pytest creator noch wij hoeven het wiel hier opnieuw uit te vinden. We kunnen elke code gewoon converteren naar AST's, die bomen bewerken, en de resultaten weer naar code converteren.

Laat me je in de volgende sectie laten zien hoe je Python's ast module gebruikt.

Hoe werkt AST, de module?

Deze ingebouwde module helpt je een AST te maken van een codestring als volgt:

(Code snippet: gebruik ast.parse om een AST te maken van code)

We geven ast.parse een string met de code die we willen parsen. Het retourneert dan de bijbehorende AST. We kunnen de boom ook in een leesbaar formaat printen met ast.dump. Hier is hoe de resulterende AST eruit ziet:

Toon de AST van je code

Trouwens, je hoeft niet eens code te schrijven om deze boom te doorlopen en te veranderen. Python heeft je gedekt met ast.NodeTransformer die precies dat doet. Maar misschien, in plaats van je te vervelen met details.

Misschien is het tijd om al deze informatie samen te brengen in de praktijk. Misschien is het demo tijd!

Het is demo tijd!

Stel, we hebben een bestand genaamd "my_test.py" met de volgende code erin:

my_test.py

Nu, als we de test_equal functie in een ander bestand importeren, laten we het "tester.py" noemen, en het uitvoeren, zal het falen, aangezien de twee getallen niet exact hetzelfde zijn.

tester.py

Wat we nu willen doen, is tester.py veranderen zodat het twee dingen doet voordat het my_test.py importeert:

Laten we beginnen met de tweede eerst. Hier is een functie die code leest en zijn AST verandert zoals vereist.

auto_round functie in tester.py

Zoals je kunt zien, parsen we de gegeven code, gebruiken dan de NodeTransformer om constanten (d.w.z. floats, integers, enz.) te vervangen door een round functie. Dan unparseren we de AST om het terug te converteren naar code.

Dan moeten we een hook schrijven die deze auto_round functie gebruikt wanneer de code in "my_test.py" wordt geïmporteerd.

Import hook in tester.py

Zodra die twee stukken code er zijn, kun je gewoon "my_test" importeren en gebruiken zoals je wilt, en de nieuwe hook en AST editor zullen ervoor zorgen dat de code wordt veranderd voordat het wordt uitgevoerd.

Ik hoop dat dit je een idee geeft over hoe pytest de AST's van je test bewerkt om de assert statements te vervangen door if statements met nuttiger outputs. Je kunt de volledige code hier bekijken voor gemakkelijker kopiëren.


Tarek Amr, 13 januari 2023

Vertalingen: [EN], [AR]