Testen is een enorm onderwerp binnen softwareontwikkeling. Voordat een softwareproduct in de handen van een eindgebruiker komt, heeft het waarschijnlijk meerdere tests doorlopen, zoals integratietests, systeemtests en acceptatietests. Het idee achter zulke grondige tests is om te zorgen dat de applicatie zich gedraagt zoals verwacht vanuit het perspectief van de eindgebruiker. Deze aanpak van testen staat bekend als behavior-driven development (BDD).
De laatste tijd is de interesse in test-driven development (TDD) onder ontwikkelaars flink toegenomen. Diep erop ingaan is te complex voor dit artikel, maar in grote lijnen komt het erop neer dat het traditionele proces van ontwikkelen en testen wordt omgedraaid: je schrijft eerst je unit tests en implementeert daarna codewijzigingen totdat de tests slagen.
In dit artikel richten we ons op unit tests en specifiek op hoe je die uitvoert met een populair Python-testframework: Pytest.
Wat zijn unit tests?
Unit tests zijn een vorm van geautomatiseerde tests — dat betekent simpelweg dat het testplan door een script wordt uitgevoerd in plaats van handmatig door een mens. Ze vormen het eerste niveau van softwaretesten en worden meestal geschreven als functies die het gedrag van verschillende functionaliteiten binnen een softwareprogramma valideren.
De niveaus van softwaretesten
Het idee achter deze tests is dat ontwikkelaars de kleinste logische code-eenheid kunnen isoleren en testen of die zich gedraagt zoals verwacht. Met andere woorden: unit tests valideren dat één component van een softwareprogramma werkt zoals de ontwikkelaars hebben bedoeld.
Ideaal gezien zijn deze tests vrij klein — hoe kleiner, hoe beter. Een reden om kleinere tests te maken is dat de test efficiënter is, omdat het testen van kleinere units ervoor zorgt dat de testcode veel sneller kan worden uitgevoerd. Een andere reden om kleinere componenten te testen is dat je beter inzicht krijgt in hoe de fijnmazige code zich gedraagt wanneer die wordt samengevoegd.
Waarom hebben we unit tests nodig?
De algemene reden waarom unit tests essentieel zijn, is dat ontwikkelaars moeten garanderen dat de code die ze schrijven aan de kwaliteitsnormen voldoet voordat die in een productieomgeving terechtkomt. Er zijn echter meerdere factoren die bijdragen aan de noodzaak van unit tests. Laten we een paar van die redenen uitdiepen.
Spaart middelen
Het uitvoeren van unit tests helpt ontwikkelaars bugs te vinden tijdens de bouwfase van software, waardoor ze niet verder in de ontwikkelcyclus doordringen. Dit bespaart middelen, omdat ontwikkelaars later in het traject niet de kosten hoeven te betalen voor het oplossen van bugs — en het betekent ook dat eindgebruikers minder snel met buggy code te maken krijgen.
Extra documentatie
Nog een goede reden om unit tests uit te voeren, is dat ze dienen als een extra laag levende documentatie voor je softwareproduct. Ontwikkelaars kunnen simpelweg naar de unit tests kijken voor een holistisch begrip van het totale systeem, omdat ze laten zien hoe de kleinere componenten zich zouden moeten gedragen.
Vertrouwensboost
Het is extreem eenvoudig om subtiele fouten in je code te maken terwijl je functionaliteit schrijft. De meeste ontwikkelaars zullen het er echter over eens zijn dat het veel beter is om de breekpunten binnen een codebase te identificeren voordat die naar productie gaat: unit tests bieden die mogelijkheid.
Het is fair om te zeggen dat “code die door unit tests wordt gedekt, betrouwbaarder kan worden beschouwd dan code die dat niet is.” Toekomstige breuken in de code worden veel sneller ontdekt dan bij code zonder testdekking, wat tijd en geld bespaart. Ontwikkelaars profiteren ook van extra documentatie, zodat ze de codebase sneller begrijpen, en er is de extra zekerheid dat fouten in hun code door de unit tests worden opgevangen in plaats van door een eindgebruiker.
Python-testframeworks
Python is door de jaren heen enorm in populariteit gegroeid. Als onderdeel daarvan is ook het aantal testframeworks toegenomen, met als resultaat een rijkdom aan tools om je Python-code te testen. De details van elk hulpmiddel uitpluizen valt buiten de scope van dit artikel, maar we stippen enkele van de meest gangbare Python-testframeworks aan.
unittest
Unittest is een ingebouwd Python-framework voor unit testing. Het is geïnspireerd op het unit testframework JUnit uit de programmeertaal Java. Omdat het standaard bij Python wordt geleverd, hoef je geen extra modules te installeren en gebruiken de meeste ontwikkelaars het om te beginnen met testen.
Pytest
Pytest is mogelijk het meest gebruikte Python-testframework — het heeft dus een grote community die je kan helpen wanneer je vastloopt. Het is een open-source framework waarmee ontwikkelaars eenvoudige, compacte testsuites kunnen schrijven, met ondersteuning voor unit testing, functioneel testen en API-testen.
doctest
Het doctest-framework combineert twee kernonderdelen van software-engineering: documentatie en testen. Deze functionaliteit zorgt ervoor dat alle softwareprogramma’s grondig worden gedocumenteerd en getest, zodat ze draaien zoals het hoort. doctest maakt deel uit van de standaardbibliotheek van Python en is vrij eenvoudig te leren.
nose2
Nose2, de opvolger van het nose-regiment, is in essentie unittest met plug-ins. Vaak wordt nose2 “uitgebreide unit tests” of “unit tests met een plug-in” genoemd vanwege de nauwe banden met het ingebouwde unittest-framework van Python. Omdat het praktisch een extensie van unittest is, is nose2 bijzonder eenvoudig te adopteren voor wie unittest al kent.
Testify
Testify, een Python-framework voor unit-, integratie- en systeemtesten, staat bekend als het framework dat ontworpen is om unittest en nose te vervangen. Het framework zit vol met uitgebreide plug-ins en heeft een vrij vlakke leercurve als je unittest al gewend bent.
Hypothesis
Hypothesis stelt ontwikkelaars in staat unit tests te maken die eenvoudiger te schrijven zijn en krachtig bij uitvoering. Omdat het framework is gebouwd met datasienceprojecten in gedachten, helpt het randgevallen te vinden die niet zo voor de hand liggen tijdens het schrijven van je tests, door voorbeelden van inputs te genereren die passen bij specifieke eigenschappen die je definieert.
Voor onze tutorial gebruiken we pytest. Bekijk de volgende sectie om te zien waarom je Pytest zou verkiezen boven de andere die we hebben genoemd.
Waarom Pytest gebruiken?
Naast de grote, helpende community heeft pytest meerdere eigenschappen die het tot een van de beste tools maken om je geautomatiseerde testsuite in Python uit te voeren. De filosofie en functies van Pytest zijn erop gericht om softwaretesten een veel betere ontwikkelaarservaring te maken. Een manier waarop de makers van Pytest dit doel bereikten, is door de hoeveelheid code die nodig is voor veelvoorkomende taken aanzienlijk te verminderen en het mogelijk te maken geavanceerde taken uit te voeren met uitgebreide commando’s en plug-ins.
Andere redenen om Pytest te gebruiken zijn onder meer:
Makkelijk te leren
Pytest is ontzettend makkelijk te leren: als je begrijpt hoe het assert-trefwoord van Python werkt, ben je al een heel eind op weg om het framework onder de knie te krijgen. Tests met pytest zijn Python-functies met “test_” ervoor of “_test” erachter in de functienaam — al kun je ook een class gebruiken om meerdere tests te groeperen. Al met al is de leercurve van pytest veel vlakker dan die van unittest, omdat je geen nieuwe constructies hoeft te leren.
Testfiltering
Je wilt mogelijk niet bij elke uitvoering al je tests draaien — zeker niet naarmate je testsuite groeit. Soms wil je een paar tests rond een nieuwe feature isoleren om tijdens het ontwikkelen snel feedback te krijgen, en daarna de volledige suite draaien zodra je vertrouwen hebt dat alles werkt zoals gepland. Pytest heeft drie manieren om tests te isoleren: 1) naamgebaseerde filtering, waarbij je pytest opdraagt alleen de tests te draaien waarvan de namen overeenkomen met het opgegeven patroon 2) directory-scoping, een standaardinstelling die pytest vertelt alleen tests te draaien die zich in of onder de huidige directory bevinden en 3) testcategorisatie, waarmee je categorieën kunt definiëren voor tests die pytest moet opnemen of uitsluiten.
Parameterization
Pytest heeft een ingebouwde decorator genaamd parametrize die het mogelijk maakt argumenten voor een testfunctie te parametriseren. Dus als de functies die je test data verwerken of een generieke transformatie uitvoeren, hoef je niet meerdere soortgelijke tests te schrijven. Later in het artikel gaan we dieper in op parametrisatie.
We laten het hier even bij, maar de lijst met redenen waarom pytest een geweldige tool is voor je geautomatiseerde testsuite gaat nog wel even door.
Pytest vs unittest
Ondanks alle bovenstaande redenen kun je nog steeds twijfelen over het gebruik van pytest om de simpele reden dat het een third-party framework is — “waarom een framework installeren als er al eentje ingebouwd is?” Dat is een valide punt, maar om ons te wapenen in die discussie geven we je een paar overwegingen mee.
Let op: Ben je al overtuigd van pytest, ga dan door naar de volgende sectie waarin we praktisch aan de slag gaan met het framework.
Minder boilerplate
Unittest vereist dat ontwikkelaars classes maken die zijn afgeleid van de TestCase-module en vervolgens de testgevallen als methoden in de class definiëren.
"""
An example test case with unittest.
See: https://docs.python.org/3/library/unittest.html
"""import unittest
classTestStringMethods(unittest.TestCase):deftest_upper(self):
self.assertEqual('foo'.upper(),'FOO')deftest_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())deftest_split(self):
s ='hello world'
self.assertEqual(s.split(),['hello','world'])# check that s.split fails when the separator is not a stringwith self.assertRaises(TypeError):
s.split(2)Mogelijk gemaakt door
Pytest daarentegen vereist alleen dat je een functie definieert met “test_” ervoor en daarin assert-voorwaarden gebruikt.
"""
An example test case with pytest.
See: https://docs.pytest.org/en/6.2.x/index.html
"""# content of test_sample.pydefinc(x):return x +1deftest_answer():assert inc(3)==5Mogelijk gemaakt door
Let op het verschil in de hoeveelheid benodigde code; unittest heeft substantieel veel boilerplate nodig die als minimumvereiste geldt voor elke test die je wil uitvoeren. Daardoor is de kans groot dat je dezelfde code meerdere keren schrijft. Pytest heeft daarentegen rijke ingebouwde features die deze workflow vereenvoudigen door de hoeveelheid code die nodig is voor testcases terug te dringen.
Output
De output van beide frameworks is erg verschillend. Hier is een voorbeeld van een pytest-uitvoering:
"""
See: https://docs.pytest.org/en/6.2.x/index.html
"""
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item
test_sample.py F [100%]================================= FAILURES =================================
_______________________________ test_answer ________________________________
deftest_answer():>assert inc(3)==5
E assert4==5
E + where 4= inc(3)
test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer -assert4==5============================1 failed in0.12s =============================Mogelijk gemaakt door
De test hierboven is gefaald, maar let op hoe gedetailleerd de uiteenzetting van de fout is. Dit maakt het makkelijker voor ontwikkelaars om te vinden waar de bugs in hun code zitten — superhandig bij het debuggen. Als extraatje is er ook een algemene statusrapportage van de testsuite die aangeeft hoeveel tests zijn gefaald en hoe lang het duurde.
Bekijk nu een voorbeeld van een gefaalde test met unittest.
import unittest
defsquare(n):return n*n
defcube(n):return n*n*n
classTestCase(unittest.TestCase):deftest_square(self):
self.asserEquals(square(4),16)deftest_cube(self):
self.assertEquals(cube(4),16)Mogelijk gemaakt door
Als we het script draaien, krijgen we de volgende output:
---------------------------------------------------------------
Ran 2 tests in0.001s
FAILED (errors=1)Mogelijk gemaakt door
De output hierboven vertelt ons dat er twee tests zijn gedraaid in 0,001 s en dat er één faalde, maar verder niet veel. Al met al geeft pytest veel informatiever feedback, wat goed van pas komt wanneer je moet debuggen.
Uiteindelijk zijn zowel pytest als unittest prima tools voor geautomatiseerd testen in Python. Veel Python-ontwikkelaars neigen echter naar pytest vanwege de compactheid en efficiëntie. Het is ook extreem eenvoudig te adopteren en er zijn allerlei features waarmee je een effectieve testsuite kunt bouwen.
Dan nu het hoofdonderdeel van dit artikel. We hebben besproken wat unit testing is en waarom pytest een uitstekende tool is voor geautomatiseerd testen in Python. Laten we nu kijken hoe je de tool gebruikt.
Pytest-tutorial
Kijken we eens hoe dit Python-testframework waar we het steeds over hebben, werkt.
De eerste stap is het installeren van het pakket, wat kan met een eenvoudige pip-opdracht.
Let op: de makers van pytest raden aan om venv te gebruiken voor ontwikkeling en pip voor het installeren van je applicatie, dependencies en pytest zelf.
Alles is geïnstalleerd. Je kunt nu tests gaan draaien.
Een simpele test maken
Een test maken is eenvoudig met Pytest. Om dit te demonstreren hebben we een script gemaakt genaamd calcualte_age.py. Dit script heeft slechts één functie, get_age, die de leeftijd van een gebruiker berekent op basis van zijn of haar geboortedatum.
import datetime
defget_age(yyyy:int, mm:int, dd:int)->int:
dob = datetime.date(yyyy, mm, dd)
today = datetime.date.today()
age =round((today - dob).days /365.25)return ageMogelijk gemaakt door
Pytest voert alle Python-bestanden uit waarvan de bestandsnaam begint met test_ of eindigt op _test. Meer specifiek volgt pytest de volgende conventies voor het ontdekken van tests [bron: documentatie]:
Als er geen argumenten zijn opgegeven, begint pytest te verzamelen in testpaths als die zijn geconfigureerd: testpaths zijn een lijst met mappen die pytest doorzoekt wanneer er geen specifieke mappen, bestanden of test-id’s zijn opgegeven.
Pytest doorloopt vervolgens recursief mappen, tenzij je hebt ingesteld dat dit niet moet met norecursedirs; het zoekt naar bestanden die beginnen met test_*.py of eindigen op *_test.py
In die bestanden verzamelt pytest test-items in de volgende volgorde:
Voorvoegsel-testfuncties of -methoden buiten een class
Voorvoegsel-testfuncties of -methoden binnen Test-voorvoegsel testclasses die geen __init__-methode hebben.
We hebben geen argumenten opgegeven, maar we hebben in dezelfde map een ander script gemaakt genaamd test_calculate_age.py: dus wanneer de mappen recursief worden doorlopen, wordt de test gevonden. In dit script hebben we één test, test_get_age, om te valideren dat onze functie correct werkt.
Let op: Je kunt er ook voor kiezen je tests in een aparte map buiten je applicatie te zetten — een goed idee als je meerdere functionele tests hebt of als je testcode en applicatiecode om een andere reden gescheiden wilt houden.
from calculate_age import get_age
deftest_get_age():# Given.
yyyy, mm, dd =map(int,"1996/07/11".split(""))# When.
age = get_age(yyyy, mm, dd)# Then.assert age ==26Mogelijk gemaakt door
Voer de test uit met het volgende commando in de commandoregel:
Maar wat als we data moeten aanleveren om onze tests te laten slagen? Maak kennis met pytest fixtures.
Pytest Fixtures
De onschatbare fixtures van Pytest stellen ontwikkelaars in staat om data in de tests te voeren. Het zijn in wezen functies die vóór elke testfunctie draaien om de staat van onze tests te beheren. Stel dat we meerdere tests hebben die allemaal dezelfde data gebruiken; dan kunnen we met een fixture de herhaalde data ophalen via één enkele functie.
import pytest
from sklearn.model_selection import train_test_split
from fraud_detection_model.config.core import config
from fraud_detection_model.processing.data_manager import(
load_datasets,
load_interim_data
)@pytest.fixture(scope="session")defpipeline_inputs():# import the training dataset
dataset = load_datasets(transaction=config.app_config.train_transaction, identity=config.app_config.train_identity,)# divide train and test
X_train, X_test, y_train, y_test = train_test_split(
dataset[config.model_config.all_features], dataset[config.model_config.target],
test_size=config.model_config.test_size,
stratify=dataset[config.model_config.target],
random_state=config.model_config.random_state)return X_train, X_test, y_train, y_testMogelijk gemaakt door
In de code hierboven hebben we een fixture gemaakt met de pytest.fixture-decorator. Let op: de scope staat op "session" om pytest te laten weten dat we willen dat de fixture aan het einde van de testsessie wordt vernietigd.
De code gebruikt een functie die we uit een andere module hebben geïmporteerd om twee csv-bestanden in het geheugen te laden en samen te voegen tot één dataset. Vervolgens wordt de dataset gesplitst in train- en testsets en geretourneerd vanuit de functie.
Om deze fixture in onze tests te gebruiken, moeten we die als parameter aan onze testfunctie meegeven. In dit voorbeeld gebruiken we onze fixture in ons testscript test_pipeline.py als volgt:
import pandas as pd
from pandas.api.types import is_categorical_dtype, is_object_dtype
from fraud_detection_model import pipeline
from fraud_detection_model.config.core import configfrom fraud_detection_model.processing.validation import validate_inputs
deftest_pipeline_most_frequent_imputer(pipeline_inputs):# Given
X_train, _, _, _ = pipeline_inputs assertall( x in X_train.loc[:, X_train.isnull().any()].columns for x in config.model_config.impute_most_freq_cols )# When
X_transformed = pipeline.fraud_detection_pipe[:1].fit_transform(X_train[:50])# Thenassertall( x notin X_transformed.loc[:, X_transformed.isnull().any()].columns for x in config.model_config.impute_most_freq_cols )deftest_pipeline_aggregate_categorical(pipeline_inputs):# Given
X_train, _, _, _ = pipeline_inputs
assert X_train["R_emaildomain"].nunique()==60# When
X_transformed = pipeline.fraud_detection_pipe[:2].fit_transform(X_train[:50])# Thenassert X_transformed["R_emaildomain"].nunique()==2Mogelijk gemaakt door
Maak je niet te druk om wat de code doet. Het belangrijkste is hoe we de noodzaak om dubbele code te schrijven flink hebben teruggebracht door een fixture te maken die we als parameter doorgeven om data in onze tests te trekken.
Er zijn echter situaties waarin fixtures wat overdreven zijn. Bijvoorbeeld als de data die in je tests wordt gehaald in elke testcase opnieuw moet worden verwerkt — dat is praktisch gelijk aan je code vervuilen met allerlei losse objecten. Dat gezegd hebbende, fixtures zullen waarschijnlijk een sleutelrol spelen binnen je testsuite, maar bepalen wanneer je ze gebruikt of vermijdt, vergt oefening en nadenken.
Pytest parametrize
Fixtures zijn geweldig wanneer je meerdere tests met dezelfde inputs hebt. Maar wat als je één functie wilt testen met kleine variaties in de inputs? Een oplossing is om meerdere verschillende tests te schrijven met uiteenlopende cases.
deftest_eval_addition():asserteval("2 + 2")==4deftest_eval_subtraction():asserteval("2 - 2")==0deftest_eval_multiplication():asserteval("2 * 2")==4deftest_eval_division():asserteval("2 / 2")==1.0Mogelijk gemaakt door
Hoewel deze oplossing zeker werkt, is het niet de meest efficiënte: om te beginnen zit er veel boilerplate in. Een betere oplossing is de decorator pytest.mark.parametrize() te gebruiken om argumenten voor een testfunctie te parametriseren. Daarmee kunnen we één enkele testdefinitie maken, waarna pytest de verschillende parameters die we opgeven voor ons test.
Zo zouden we de bovenstaande code herschrijven met parametrisatie:
import pytest
@pytest.mark.parametrize("test_input, expected_output",[("2+2",4),("2-2",0),("2*2",4),("2/2",1.0)])deftest_eval(test_input, expected_output):asserteval(test_input)== expected_outputMogelijk gemaakt door
De @parametrize-decorator definieert vier verschillende testinputs en verwachte waarden voor de functie test_eval — dat betekent dat de functie vier keer draait, telkens met één van de sets.
De output van pytest voor een parametrized test
In dit artikel hebben we het volgende behandeld:
wat unit tests zijn
waarom we unit tests nodig hebben
verschillende testframeworks in Python
waarom pytest zo nuttig is
hoe je pytest gebruikt en twee van de belangrijkste features (fixtures & parametrisatie)
Je weet nu genoeg om je eigen tests te schrijven met het pytest-framework. We raden je sterk aan dat te doen, zodat alles wat je in dit artikel hebt geleerd, blijft hangen. Bekijk ook onze cursus unit testing for data science in python voor een uitgebreidere uitleg met voorbeelden.
Duik in datawetenschap met Python en leer hoe je je data goed kunt analyseren en visualiseren. Je hoeft niet te kunnen programmeren of iets dergelijks.
Testen is een enorm onderwerp binnen softwareontwikkeling. Voordat een softwareproduct in de handen van een eindgebruiker komt, heeft het waarschijnlijk meerdere tests doorlopen, zoals integratietests, systeemtests en acceptatietests. Het idee achter zulke grondige tests is om te zorgen dat de applicatie zich gedraagt zoals verwacht vanuit het perspectief van de eindgebruiker. Deze aanpak van testen staat bekend als behavior-driven development (BDD).
De laatste tijd is de interesse in test-driven development (TDD) onder ontwikkelaars flink toegenomen. Diep erop ingaan is te complex voor dit artikel, maar in grote lijnen komt het erop neer dat het traditionele proces van ontwikkelen en testen wordt omgedraaid: je schrijft eerst je unit tests en implementeert daarna codewijzigingen totdat de tests slagen.
In dit artikel richten we ons op unit tests en specifiek op hoe je die uitvoert met een populair Python-testframework: Pytest.
Wat zijn unit tests?
Unit tests zijn een vorm van geautomatiseerde tests — dat betekent simpelweg dat het testplan door een script wordt uitgevoerd in plaats van handmatig door een mens. Ze vormen het eerste niveau van softwaretesten en worden meestal geschreven als functies die het gedrag van verschillende functionaliteiten binnen een softwareprogramma valideren.
De niveaus van softwaretesten
Het idee achter deze tests is dat ontwikkelaars de kleinste logische code-eenheid kunnen isoleren en testen of die zich gedraagt zoals verwacht. Met andere woorden: unit tests valideren dat één component van een softwareprogramma werkt zoals de ontwikkelaars hebben bedoeld.
Ideaal gezien zijn deze tests vrij klein — hoe kleiner, hoe beter. Een reden om kleinere tests te maken is dat de test efficiënter is, omdat het testen van kleinere units ervoor zorgt dat de testcode veel sneller kan worden uitgevoerd. Een andere reden om kleinere componenten te testen is dat je beter inzicht krijgt in hoe de fijnmazige code zich gedraagt wanneer die wordt samengevoegd.
Waarom hebben we unit tests nodig?
De algemene reden waarom unit tests essentieel zijn, is dat ontwikkelaars moeten garanderen dat de code die ze schrijven aan de kwaliteitsnormen voldoet voordat die in een productieomgeving terechtkomt. Er zijn echter meerdere factoren die bijdragen aan de noodzaak van unit tests. Laten we een paar van die redenen uitdiepen.
Spaart middelen
Het uitvoeren van unit tests helpt ontwikkelaars bugs te vinden tijdens de bouwfase van software, waardoor ze niet verder in de ontwikkelcyclus doordringen. Dit bespaart middelen, omdat ontwikkelaars later in het traject niet de kosten hoeven te betalen voor het oplossen van bugs — en het betekent ook dat eindgebruikers minder snel met buggy code te maken krijgen.
Extra documentatie
Nog een goede reden om unit tests uit te voeren, is dat ze dienen als een extra laag levende documentatie voor je softwareproduct. Ontwikkelaars kunnen simpelweg naar de unit tests kijken voor een holistisch begrip van het totale systeem, omdat ze laten zien hoe de kleinere componenten zich zouden moeten gedragen.
Vertrouwensboost
Het is extreem eenvoudig om subtiele fouten in je code te maken terwijl je functionaliteit schrijft. De meeste ontwikkelaars zullen het er echter over eens zijn dat het veel beter is om de breekpunten binnen een codebase te identificeren voordat die naar productie gaat: unit tests bieden die mogelijkheid.
Het is fair om te zeggen dat “code die door unit tests wordt gedekt, betrouwbaarder kan worden beschouwd dan code die dat niet is.” Toekomstige breuken in de code worden veel sneller ontdekt dan bij code zonder testdekking, wat tijd en geld bespaart. Ontwikkelaars profiteren ook van extra documentatie, zodat ze de codebase sneller begrijpen, en er is de extra zekerheid dat fouten in hun code door de unit tests worden opgevangen in plaats van door een eindgebruiker.
Python-testframeworks
Python is door de jaren heen enorm in populariteit gegroeid. Als onderdeel daarvan is ook het aantal testframeworks toegenomen, met als resultaat een rijkdom aan tools om je Python-code te testen. De details van elk hulpmiddel uitpluizen valt buiten de scope van dit artikel, maar we stippen enkele van de meest gangbare Python-testframeworks aan.
unittest
Unittest is een ingebouwd Python-framework voor unit testing. Het is geïnspireerd op het unit testframework JUnit uit de programmeertaal Java. Omdat het standaard bij Python wordt geleverd, hoef je geen extra modules te installeren en gebruiken de meeste ontwikkelaars het om te beginnen met testen.
Pytest
Pytest is mogelijk het meest gebruikte Python-testframework — het heeft dus een grote community die je kan helpen wanneer je vastloopt. Het is een open-source framework waarmee ontwikkelaars eenvoudige, compacte testsuites kunnen schrijven, met ondersteuning voor unit testing, functioneel testen en API-testen.
doctest
Het doctest-framework combineert twee kernonderdelen van software-engineering: documentatie en testen. Deze functionaliteit zorgt ervoor dat alle softwareprogramma’s grondig worden gedocumenteerd en getest, zodat ze draaien zoals het hoort. doctest maakt deel uit van de standaardbibliotheek van Python en is vrij eenvoudig te leren.
nose2
Nose2, de opvolger van het nose-regiment, is in essentie unittest met plug-ins. Vaak wordt nose2 “uitgebreide unit tests” of “unit tests met een plug-in” genoemd vanwege de nauwe banden met het ingebouwde unittest-framework van Python. Omdat het praktisch een extensie van unittest is, is nose2 bijzonder eenvoudig te adopteren voor wie unittest al kent.
Testify
Testify, een Python-framework voor unit-, integratie- en systeemtesten, staat bekend als het framework dat ontworpen is om unittest en nose te vervangen. Het framework zit vol met uitgebreide plug-ins en heeft een vrij vlakke leercurve als je unittest al gewend bent.
Hypothesis
Hypothesis stelt ontwikkelaars in staat unit tests te maken die eenvoudiger te schrijven zijn en krachtig bij uitvoering. Omdat het framework is gebouwd met datasienceprojecten in gedachten, helpt het randgevallen te vinden die niet zo voor de hand liggen tijdens het schrijven van je tests, door voorbeelden van inputs te genereren die passen bij specifieke eigenschappen die je definieert.
Voor onze tutorial gebruiken we pytest. Bekijk de volgende sectie om te zien waarom je Pytest zou verkiezen boven de andere die we hebben genoemd.
Waarom Pytest gebruiken?
Naast de grote, helpende community heeft pytest meerdere eigenschappen die het tot een van de beste tools maken om je geautomatiseerde testsuite in Python uit te voeren. De filosofie en functies van Pytest zijn erop gericht om softwaretesten een veel betere ontwikkelaarservaring te maken. Een manier waarop de makers van Pytest dit doel bereikten, is door de hoeveelheid code die nodig is voor veelvoorkomende taken aanzienlijk te verminderen en het mogelijk te maken geavanceerde taken uit te voeren met uitgebreide commando’s en plug-ins.
Andere redenen om Pytest te gebruiken zijn onder meer:
Makkelijk te leren
Pytest is ontzettend makkelijk te leren: als je begrijpt hoe het assert-trefwoord van Python werkt, ben je al een heel eind op weg om het framework onder de knie te krijgen. Tests met pytest zijn Python-functies met “test_” ervoor of “_test” erachter in de functienaam — al kun je ook een class gebruiken om meerdere tests te groeperen. Al met al is de leercurve van pytest veel vlakker dan die van unittest, omdat je geen nieuwe constructies hoeft te leren.
Testfiltering
Je wilt mogelijk niet bij elke uitvoering al je tests draaien — zeker niet naarmate je testsuite groeit. Soms wil je een paar tests rond een nieuwe feature isoleren om tijdens het ontwikkelen snel feedback te krijgen, en daarna de volledige suite draaien zodra je vertrouwen hebt dat alles werkt zoals gepland. Pytest heeft drie manieren om tests te isoleren: 1) naamgebaseerde filtering, waarbij je pytest opdraagt alleen de tests te draaien waarvan de namen overeenkomen met het opgegeven patroon 2) directory-scoping, een standaardinstelling die pytest vertelt alleen tests te draaien die zich in of onder de huidige directory bevinden en 3) testcategorisatie, waarmee je categorieën kunt definiëren voor tests die pytest moet opnemen of uitsluiten.
Parameterization
Pytest heeft een ingebouwde decorator genaamd parametrize die het mogelijk maakt argumenten voor een testfunctie te parametriseren. Dus als de functies die je test data verwerken of een generieke transformatie uitvoeren, hoef je niet meerdere soortgelijke tests te schrijven. Later in het artikel gaan we dieper in op parametrisatie.
We laten het hier even bij, maar de lijst met redenen waarom pytest een geweldige tool is voor je geautomatiseerde testsuite gaat nog wel even door.
Pytest vs unittest
Ondanks alle bovenstaande redenen kun je nog steeds twijfelen over het gebruik van pytest om de simpele reden dat het een third-party framework is — “waarom een framework installeren als er al eentje ingebouwd is?” Dat is een valide punt, maar om ons te wapenen in die discussie geven we je een paar overwegingen mee.
Let op: Ben je al overtuigd van pytest, ga dan door naar de volgende sectie waarin we praktisch aan de slag gaan met het framework.
Minder boilerplate
Unittest vereist dat ontwikkelaars classes maken die zijn afgeleid van de TestCase-module en vervolgens de testgevallen als methoden in de class definiëren.
Pytest daarentegen vereist alleen dat je een functie definieert met “test_” ervoor en daarin assert-voorwaarden gebruikt.
Let op het verschil in de hoeveelheid benodigde code; unittest heeft substantieel veel boilerplate nodig die als minimumvereiste geldt voor elke test die je wil uitvoeren. Daardoor is de kans groot dat je dezelfde code meerdere keren schrijft. Pytest heeft daarentegen rijke ingebouwde features die deze workflow vereenvoudigen door de hoeveelheid code die nodig is voor testcases terug te dringen.
Output
De output van beide frameworks is erg verschillend. Hier is een voorbeeld van een pytest-uitvoering:
De test hierboven is gefaald, maar let op hoe gedetailleerd de uiteenzetting van de fout is. Dit maakt het makkelijker voor ontwikkelaars om te vinden waar de bugs in hun code zitten — superhandig bij het debuggen. Als extraatje is er ook een algemene statusrapportage van de testsuite die aangeeft hoeveel tests zijn gefaald en hoe lang het duurde.
Bekijk nu een voorbeeld van een gefaalde test met unittest.
Als we het script draaien, krijgen we de volgende output:
De output hierboven vertelt ons dat er twee tests zijn gedraaid in 0,001 s en dat er één faalde, maar verder niet veel. Al met al geeft pytest veel informatiever feedback, wat goed van pas komt wanneer je moet debuggen.
Uiteindelijk zijn zowel pytest als unittest prima tools voor geautomatiseerd testen in Python. Veel Python-ontwikkelaars neigen echter naar pytest vanwege de compactheid en efficiëntie. Het is ook extreem eenvoudig te adopteren en er zijn allerlei features waarmee je een effectieve testsuite kunt bouwen.
Dan nu het hoofdonderdeel van dit artikel. We hebben besproken wat unit testing is en waarom pytest een uitstekende tool is voor geautomatiseerd testen in Python. Laten we nu kijken hoe je de tool gebruikt.
Pytest-tutorial
Kijken we eens hoe dit Python-testframework waar we het steeds over hebben, werkt.
De eerste stap is het installeren van het pakket, wat kan met een eenvoudige pip-opdracht.
Let op: de makers van pytest raden aan om venv te gebruiken voor ontwikkeling en pip voor het installeren van je applicatie, dependencies en pytest zelf.
Controleer daarna of het framework is geïnstalleerd met het volgende commando:
Alles is geïnstalleerd. Je kunt nu tests gaan draaien.
Een simpele test maken
Een test maken is eenvoudig met Pytest. Om dit te demonstreren hebben we een script gemaakt genaamd calcualte_age.py. Dit script heeft slechts één functie, get_age, die de leeftijd van een gebruiker berekent op basis van zijn of haar geboortedatum.
Pytest voert alle Python-bestanden uit waarvan de bestandsnaam begint met test_ of eindigt op _test. Meer specifiek volgt pytest de volgende conventies voor het ontdekken van tests [bron: documentatie]:
We hebben geen argumenten opgegeven, maar we hebben in dezelfde map een ander script gemaakt genaamd test_calculate_age.py: dus wanneer de mappen recursief worden doorlopen, wordt de test gevonden. In dit script hebben we één test, test_get_age, om te valideren dat onze functie correct werkt.
Let op: Je kunt er ook voor kiezen je tests in een aparte map buiten je applicatie te zetten — een goed idee als je meerdere functionele tests hebt of als je testcode en applicatiecode om een andere reden gescheiden wilt houden.
Voer de test uit met het volgende commando in de commandoregel:
De output van een succesvolle pytest-run.
En dat is alles.
Maar wat als we data moeten aanleveren om onze tests te laten slagen? Maak kennis met pytest fixtures.
Pytest Fixtures
De onschatbare fixtures van Pytest stellen ontwikkelaars in staat om data in de tests te voeren. Het zijn in wezen functies die vóór elke testfunctie draaien om de staat van onze tests te beheren. Stel dat we meerdere tests hebben die allemaal dezelfde data gebruiken; dan kunnen we met een fixture de herhaalde data ophalen via één enkele functie.
In de code hierboven hebben we een fixture gemaakt met de pytest.fixture-decorator. Let op: de scope staat op "session" om pytest te laten weten dat we willen dat de fixture aan het einde van de testsessie wordt vernietigd.
Let op: onze fixtures staan in conftest.py.
De code gebruikt een functie die we uit een andere module hebben geïmporteerd om twee csv-bestanden in het geheugen te laden en samen te voegen tot één dataset. Vervolgens wordt de dataset gesplitst in train- en testsets en geretourneerd vanuit de functie.
Om deze fixture in onze tests te gebruiken, moeten we die als parameter aan onze testfunctie meegeven. In dit voorbeeld gebruiken we onze fixture in ons testscript test_pipeline.py als volgt:
Maak je niet te druk om wat de code doet. Het belangrijkste is hoe we de noodzaak om dubbele code te schrijven flink hebben teruggebracht door een fixture te maken die we als parameter doorgeven om data in onze tests te trekken.
Er zijn echter situaties waarin fixtures wat overdreven zijn. Bijvoorbeeld als de data die in je tests wordt gehaald in elke testcase opnieuw moet worden verwerkt — dat is praktisch gelijk aan je code vervuilen met allerlei losse objecten. Dat gezegd hebbende, fixtures zullen waarschijnlijk een sleutelrol spelen binnen je testsuite, maar bepalen wanneer je ze gebruikt of vermijdt, vergt oefening en nadenken.
Pytest parametrize
Fixtures zijn geweldig wanneer je meerdere tests met dezelfde inputs hebt. Maar wat als je één functie wilt testen met kleine variaties in de inputs? Een oplossing is om meerdere verschillende tests te schrijven met uiteenlopende cases.
Hoewel deze oplossing zeker werkt, is het niet de meest efficiënte: om te beginnen zit er veel boilerplate in. Een betere oplossing is de decorator pytest.mark.parametrize() te gebruiken om argumenten voor een testfunctie te parametriseren. Daarmee kunnen we één enkele testdefinitie maken, waarna pytest de verschillende parameters die we opgeven voor ons test.
Zo zouden we de bovenstaande code herschrijven met parametrisatie:
De @parametrize-decorator definieert vier verschillende testinputs en verwachte waarden voor de functie test_eval — dat betekent dat de functie vier keer draait, telkens met één van de sets.
De output van pytest voor een parametrized test
In dit artikel hebben we het volgende behandeld:
Je weet nu genoeg om je eigen tests te schrijven met het pytest-framework. We raden je sterk aan dat te doen, zodat alles wat je in dit artikel hebt geleerd, blijft hangen. Bekijk ook onze cursus unit testing for data science in python voor een uitgebreidere uitleg met voorbeelden.