Unit Testing in Python: Compleet gids met voorbeelden
Python-testen met pytest, TDD, mocking en coverage
Eenheidstesten zorgen ervoor dat je Python-code correct werkt en blijft werken terwijl je project evolueert. Deze uitgebreide gids behandelt alles wat je moet weten over eenheidstesten in Python, van basisconcepten tot geavanceerde technieken.

Waarom Eenheidstesten Belangrijk zijn
Eenheidstesten bieden veel voordelen voor Python-ontwikkelaars:
- Vroegtijdige foutdetectie: Fouten opvangen voordat ze in productie komen
- Codekwaliteit: Dwingen je om modulaire, testbare code te schrijven
- Vertrouwen bij herschrijven: Veranderingen maken met zekerheid, wetend dat tests regressies opvangen
- Documentatie: Tests dienen als uitvoerbare documentatie van hoe code moet werken
- Snellere ontwikkeling: Automatische tests zijn sneller dan handmatige testen
- Beter ontwerp: Het schrijven van testbare code leidt tot een betere architectuur
Begrijpen van de Fundamenten van Eenheidstesten
Wat is een Eenheidstest?
Een eenheidstest verifieert de kleinste testbare onderdeel van een applicatie (meestal een functie of methode) in isolatie. Het moet zijn:
- Snel: Uitgevoerd in milliseconden
- Isolatie: Onafhankelijk van andere tests en externe systemen
- Herhaalbaar: Hetzelfde resultaat opleveren elke keer
- Zelfverificerend: Duidelijk passen of falen zonder handmatige inspectie
- Tijdig: Geschreven voor of naast de code
De Testpiramide
Een gezonde testsuite volgt de testpiramide:
/\
/ \ E2E Tests (Weinig)
/____\
/ \ Integratie Tests (Een aantal)
/_____ ___\
/ \ Eenheidstests (Veel)
/_____ __ ___\
Eenheidstests vormen de basis - ze zijn talrijk, snel en geven snelle feedback.
Vergelijking van Python Testframeworks
unittest: Het Ingebouwde Framework
De standaardbibliotheek van Python bevat unittest, geïnspireerd op JUnit:
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
"""Voordat elke test wordt uitgevoerd"""
self.calc = Calculator()
def tearDown(self):
"""Na elke test"""
self.calc = None
def test_addition(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
self.calc.divide(10, 0)
if __name__ == '__main__':
unittest.main()
Voordelen:
- Ingebouwd, geen installatie vereist
- Goed voor ontwikkelaars die vertrouwd zijn met xUnit-frameworks
- Ondernemingsvriendelijk, goed gevestigd
Nadelen:
- Verward syntax met boilerplate
- Vereist klassen voor testorganisatie
- Minder flexibele fixturebeheer
pytest: De Moderne Keuze
pytest is het populairste derde partij testframework:
import pytest
def test_addition():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_division_by_zero():
calc = Calculator()
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_addition_parametrized(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
Voordelen:
- Eenvoudige, Pythonische syntax
- Krachtig fixture-systeem
- Uitstekende plugin-ecosysteem
- Betere foutmeldingen
- Parametriseerde testen ingebouwd
Nadelen:
- Vereist installatie
- Minder bekend bij ontwikkelaars van andere talen
Installatie:
pip install pytest pytest-cov pytest-mock
Schrijven van Je Eerste Eenheidstests
Laten we een eenvoudig voorbeeld vanaf nul bouwen met Test-Driven Development (TDD). Als je nieuw bent in Python of een snelle verwijzing nodig hebt voor syntaxis en taalkenmerken, bekijk dan onze Python Cheat Sheet voor een uitgebreid overzicht van Python-fundamenten.
Voorbeeld: String Utility Functions
Stap 1: Schrijf de Test Eerst (Rood)
Maak test_string_utils.py:
import pytest
from string_utils import reverse_string, is_palindrome, count_vowels
def test_reverse_string():
assert reverse_string("hello") == "olleh"
assert reverse_string("") == ""
assert reverse_string("a") == "a"
def test_is_palindrome():
assert is_palindrome("racecar") == True
assert is_palindrome("hello") == False
assert is_palindrome("") == True
assert is_palindrome("A man a plan a canal Panama") == True
def test_count_vowels():
assert count_vowels("hello") == 2
assert count_vowels("HELLO") == 2
assert count_vowels("xyz") == 0
assert count_vowels("") == 0
Stap 2: Schrijf Minimale Code om Te Passen (Groen)
Maak string_utils.py:
def reverse_string(s: str) -> str:
"""Omkeer een string"""
return s[::-1]
def is_palindrome(s: str) -> bool:
"""Controleer of string een palindroom is (genegeerd hoofdletters en spaties)"""
cleaned = ''.join(s.lower().split())
return cleaned == cleaned[::-1]
def count_vowels(s: str) -> int:
"""Tel de klinkers in een string"""
return sum(1 for char in s.lower() if char in 'aeiou')
Stap 3: Test Uitvoeren
pytest test_string_utils.py -v
Uitvoer:
test_string_utils.py::test_reverse_string PASSED
test_string_utils.py::test_is_palindrome PASSED
test_string_utils.py::test_count_vowels PASSED
Geavanceerde Testtechnieken
Fixtures voor Testopstelling
Fixtures bieden herbruikbare testopstelling en -opruiming:
import pytest
from database import Database
@pytest.fixture
def db():
"""Maak een testdatabase aan"""
database = Database(":memory:")
database.create_tables()
yield database # Geef door aan de test
database.close() # Opruimen na de test
@pytest.fixture
def sample_users(db):
"""Voeg voorbeeldgebruikers toe aan de database"""
db.add_user("Alice", "alice@example.com")
db.add_user("Bob", "bob@example.com")
return db
def test_get_user(sample_users):
user = sample_users.get_user_by_email("alice@example.com")
assert user.name == "Alice"
def test_user_count(sample_users):
assert sample_users.count_users() == 2
Fixture Scopes
Controleer de levensduur van fixtures met scopes:
@pytest.fixture(scope="function") # Standaard: uitvoeren voor elke test
def func_fixture():
return create_resource()
@pytest.fixture(scope="class") # Een keer per testklasse
def class_fixture():
return create_resource()
@pytest.fixture(scope="module") # Een keer per module
def module_fixture():
return create_resource()
@pytest.fixture(scope="session") # Een keer per testsessie
def session_fixture():
return create_expensive_resource()
Mocken van Externe Afhankelijkheden
Gebruik mocken om code te isoleren van externe afhankelijkheden:
from unittest.mock import Mock, patch, MagicMock
import requests
class WeatherService:
def get_temperature(self, city):
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temp"]
# Test met mock
def test_get_temperature():
service = WeatherService()
# Mock de requests.get functie
with patch('requests.get') as mock_get:
# Configureer mock respons
mock_response = Mock()
mock_response.json.return_value = {"temp": 72}
mock_get.return_value = mock_response
# Test
temp = service.get_temperature("Boston")
assert temp == 72
# Controleer de oproep
mock_get.assert_called_once_with("https://api.weather.com/Boston")
Gebruik van pytest-mock Plugin
pytest-mock biedt een nettere syntaxis:
def test_get_temperature(mocker):
service = WeatherService()
# Mock met pytest-mock
mock_response = mocker.Mock()
mock_response.json.return_value = {"temp": 72}
mocker.patch('requests.get', return_value=mock_response)
temp = service.get_temperature("Boston")
assert temp == 72
Parametriseerde Testen
Test meerdere scenario’s efficiënt:
import pytest
@pytest.mark.parametrize("input,expected", [
("", True),
("a", True),
("ab", False),
("aba", True),
("racecar", True),
("hello", False),
])
def test_is_palindrome_parametrized(input, expected):
assert is_palindrome(input) == expected
@pytest.mark.parametrize("number,is_even", [
(0, True),
(1, False),
(2, True),
(-1, False),
(-2, True),
])
def test_is_even(number, is_even):
assert (number % 2 == 0) == is_even
Testen van Uitzonderingen en Foutafhandeling
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Kan niet delen door nul")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Kan niet delen door nul"):
divide(10, 0)
def test_divide_by_zero_with_message():
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "nul" in str(exc_info.value).lower()
# Testen dat geen uitzondering wordt opgeworpen
def test_divide_success():
result = divide(10, 2)
assert result == 5.0
Testen van Asynchrone Code
Testen van asynchrone code is essentieel voor moderne Python-applicaties, vooral bij het werken met APIs, databases of AI-diensten. Hier is hoe je asynchrone functies kunt testen:
import pytest
import asyncio
async def fetch_data(url):
"""Asynchrone functie om data op te halen"""
await asyncio.sleep(0.1) # Simuleer API-aanroep
return {"status": "success"}
@pytest.mark.asyncio
async def test_fetch_data():
result = await fetch_data("https://api.example.com")
assert result["status"] == "success"
@pytest.mark.asyncio
async def test_fetch_data_with_mock(mocker):
# Mock de asynchrone functie
mock_fetch = mocker.AsyncMock(return_value={"status": "mocked"})
mocker.patch('module.fetch_data', mock_fetch)
result = await fetch_data("https://api.example.com")
assert result["status"] == "mocked"
Voor praktische voorbeelden van het testen van asynchrone code met AI-diensten, zie onze gids over integreren van Ollama met Python, die teststrategieën voor LLM-interacties bevat.
Code Coverage
Meet hoeveel van je code getest is:
Gebruik van pytest-cov
# Testen uitvoeren met coverage
pytest --cov=myproject tests/
# HTML-rapport genereren
pytest --cov=myproject --cov-report=html tests/
# Ontbrekende regels tonen
pytest --cov=myproject --cov-report=term-missing tests/
Coverage Configuratie
Maak .coveragerc:
[run]
source = myproject
omit =
*/tests/*
*/venv/*
*/__pycache__/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
Coverage Best Practices
- Streef naar 80%+ coverage voor kritieke codepaden
- Zet niet te veel op 100% - richt je op betekenisvolle tests
- Test randgevallen niet alleen gelukkige paden
- Uitsluiten van boilerplate uit coverage-rapporten
- Gebruik coverage als een gids niet als doel
Testorganisatie en Projectstructuur
Aanbevolen Structuur
myproject/
├── myproject/
│ ├── __init__.py
│ ├── module1.py
│ ├── module2.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Gedeelde fixtures
│ ├── test_module1.py
│ ├── test_module2.py
│ ├── test_utils.py
│ └── integration/
│ ├── __init__.py
│ └── test_integration.py
├── pytest.ini
├── requirements.txt
└── requirements-dev.txt
conftest.py voor Gedeelde Fixtures
# tests/conftest.py
import pytest
from myproject.database import Database
@pytest.fixture(scope="session")
def test_db():
"""Sessiebrede testdatabase"""
db = Database(":memory:")
db.create_schema()
yield db
db.close()
@pytest.fixture
def clean_db(test_db):
"""Schoon database voor elke test"""
test_db.clear_all_tables()
return test_db
pytest.ini Configuratie
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--cov=myproject
--cov-report=term-missing
--cov-report=html
markers =
slow: merkt tests als traag
integration: merkt tests als integratiestests
unit: merkt tests als eenheidstests
Beste Praktijken voor Eenheidstesten
1. Volg het AAA Patroon
Arrange-Act-Assert maakt tests duidelijk:
def test_user_creation():
# Arrange
username = "john_doe"
email = "john@example.com"
# Act
user = User(username, email)
# Assert
assert user.username == username
assert user.email == email
assert user.is_active == True
2. Één Assertie per Test (richtlijn, niet regel)
# Goed: Gerichte test
def test_user_username():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
def test_user_email():
user = User("john_doe", "john@example.com")
assert user.email == "john@example.com"
# Ook aanvaardbaar: Gerelateerde asserties
def test_user_creation():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
assert user.email == "john@example.com"
assert isinstance(user.created_at, datetime)
3. Gebruik Beschrijvende Testnamen
# Slecht
def test_user():
pass
# Goed
def test_user_creation_with_valid_data():
pass
def test_user_creation_fails_with_invalid_email():
pass
def test_user_password_is_hashed_after_setting():
pass
4. Test Randgevallen en Grenzen
def test_age_validation():
# Geldige gevallen
assert validate_age(0) == True
assert validate_age(18) == True
assert validate_age(120) == True
# Randgevallen
assert validate_age(-1) == False
assert validate_age(121) == False
# Randgevallen
with pytest.raises(TypeError):
validate_age("18")
with pytest.raises(TypeError):
validate_age(None)
5. Houd Tests Onafhankelijk
# Slecht: Tests afhankelijk van volgorde
counter = 0
def test_increment():
global counter
counter += 1
assert counter == 1
def test_increment_again(): # Fout als alleen uitgevoerd
global counter
counter += 1
assert counter == 2
# Goed: Tests onafhankelijk
def test_increment():
counter = Counter()
counter.increment()
assert counter.value == 1
def test_increment_multiple_times():
counter = Counter()
counter.increment()
counter.increment()
assert counter.value == 2
6. Test Geen Implementatie Details
# Slecht: Test implementatie
def test_sort_uses_quicksort():
sorter = Sorter()
assert sorter.algorithm == "quicksort"
# Goed: Test gedrag
def test_sort_returns_sorted_list():
sorter = Sorter()
result = sorter.sort([3, 1, 2])
assert result == [1, 2, 3]
7. Test Reële Gebruiksgevallen
Wanneer je testbibliotheken die gegevens verwerken of transformeren, richt je op reële scenario’s. Bijvoorbeeld, als je werkt met web scraping of inhoudsconversie, bekijk dan onze gids over converteer HTML naar Markdown met Python, die teststrategieën en benchmarkvergelijkingen voor verschillende conversiebibliotheken bevat.
Integratie met Continue Integratie
Voorbeeld van GitHub Actions
# .github/workflows/tests.yml
name: Tests
op: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.10, 3.11, 3.12]
steps:
- uses: actions/checkout@v3
- name: Stel Python ${{ matrix.python-version }} in
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Installeer afhankelijkheden
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Voer tests uit
run: |
pytest --cov=myproject --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Testen van Serverless Functies
Wanneer je AWS Lambda-functies of serverless-applicaties test, overweeg dan integratieteststrategieën naast eenheidstests. Onze gids over bouwen van een dubbelmodus AWS Lambda met Python en Terraform behandelt teststrategieën voor serverless Python-applicaties, inclusief hoe je Lambda-handlers, SQS-consumers en API Gateway-integraties kunt testen.
Checklist van Beste Praktijken voor Testen
- Schrijf tests voor of naast code (TDD)
- Houd tests snel (< 1 seconde per test)
- Maak tests onafhankelijk en geïsoleerd
- Gebruik beschrijvende testnamen
- Volg het AAA patroon (Arrange-Act-Assert)
- Test randgevallen en foutomstandigheden
- Mock externe afhankelijkheden
- Streef naar 80%+ code coverage
- Voer tests uit in de CI/CD-pijplijn
- Beoordeel en herschrijf tests regelmatig
- Document complexe testscenario’s
- Gebruik fixtures voor algemene opstelling
- Parametrize gelijke tests
- Houd tests eenvoudig en leesbaar
Algemene Testpatronen
Testen van Classes
class TestUser:
@pytest.fixture
def user(self):
return User("john_doe", "john@example.com")
def test_username(self, user):
assert user.username == "john_doe"
def test_email(self, user):
assert user.email == "john@example.com"
def test_full_name(self, user):
user.first_name = "John"
user.last_name = "Doe"
assert user.full_name() == "John Doe"
Testen met Tijdelijke Bestanden
import pytest
from pathlib import Path
@pytest.fixture
def temp_file(tmp_path):
"""Maak een tijdelijk bestand aan"""
file_path = tmp_path / "test_file.txt"
file_path.write_text("test content")
return file_path
def test_read_file(temp_file):
content = temp_file.read_text()
assert content == "test content"
Testen van Bestandsopmaak
Wanneer je code test die bestanden genereert (zoals PDF’s, afbeeldingen of documenten), gebruik tijdelijke mappen en controleer bestandskenmerken:
@pytest.fixture
def temp_output_dir(tmp_path):
"""Bied een tijdelijke uitvoermap aan"""
output_dir = tmp_path / "output"
output_dir.mkdir()
return output_dir
def test_pdf_generation(temp_output_dir):
pdf_path = temp_output_dir / "output.pdf"
generate_pdf(pdf_path, content="Test")
assert pdf_path.exists()
assert pdf_path.stat().st_size > 0
Voor uitgebreide voorbeelden van het testen van PDF-generatie, zie onze gids over genereren van PDF in Python, die teststrategieën voor verschillende PDF-bibliotheken behandelt.
Testen met Monkeypatch
def test_environment_variable(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key_123")
assert os.getenv("API_KEY") == "test_key_123"
def test_module_attribute(monkeypatch):
monkeypatch.setattr("module.CONSTANT", 42)
assert module.CONSTANT == 42
Nuttige Links en Resources
- pytest Documentatie
- unittest Documentatie
- pytest-cov Plugin
- pytest-mock Plugin
- unittest.mock Documentatie
- Test Driven Development by Example - Kent Beck
- Python Testing with pytest - Brian Okken
Gerelateerde Resources
Python Fundamenten en Beste Praktijken
- Python Cheat Sheet - Uitgebreid overzicht van Python-syntaxis, datastructuren en veelvoorkomende patronen
Testen van Specifieke Python Gebruiksgevallen
- Integreren van Ollama met Python - Teststrategieën voor LLM-integraties en asynchrone AI-dienstoproepen
- Converteer HTML naar Markdown met Python - Teststrategieën voor web scraping en inhoudsconversiebibliotheken
- Genereren van PDF in Python - Teststrategieën voor PDF-generatie en bestandsuitvoer
Serverless en Cloud Testen
- Bouwen van een Dubbelmodus AWS Lambda met Python en Terraform - Teststrategieën voor Lambda-functies, SQS-consumers en API Gateway-integraties
Eenheidstesten zijn een essentieel vaardigheid voor Python-ontwikkelaars. Of je unittest of pytest kiest, het belangrijkste is om tests consistent te schrijven, ze onderhoudbaar te houden en ze te integreren in je ontwikkelingswerkstroom. Begin met eenvoudige tests, adopteer geleidelijk geavanceerde technieken zoals mocken en fixtures, en gebruik coverage-tools om ongeteste code te identificeren.