Enhetstestning i Python: Komplett guide med exempel
Python-testning med pytest, TDD, mockning och täckning
Enhetstestning säkerställer att din Python-kod fungerar korrekt och fortsätter att fungera när ditt projekt utvecklas. Denna omfattande guide täcker allt du behöver veta om enhetstestning i Python, från grundläggande begrepp till avancerade tekniker.

Varför enhetstestning är viktigt
Enhetstestning ger många fördelar för Python-utvecklare:
- Tidig felupptäckt: Fånga fel innan de når produktion
- Kodkvalitet: Tvingar dig att skriva modulär, testbar kod
- Refaktoreringstrygghet: Gör ändringar säkert med vetskap om att tester kommer fånga regressioner
- Dokumentation: Tester fungerar som exekverbar dokumentation om hur koden ska fungera
- Snabbare utveckling: Automatiserade tester är snabbare än manuell testning
- Bättre design: Att skriva testbar kod leder till bättre arkitektur
Förstå grunderna för enhetstestning
Vad är en enhetstest?
En enhetstest verifierar den minsta testbara delen av en applikation (vanligtvis en funktion eller metod) i isolation. Den ska vara:
- Snabb: Köra på millisekunder
- Isolerad: Oberoende av andra tester och externa system
- Upprepbar: Ge samma resultat varje gång
- Självvaliderande: Godkänn eller misslyckas tydligt utan manuell inspektion
- Tidig: Skriven före eller samtidigt som koden
Testpyramiden
En hälsosam testsuite följer testpyramiden:
/\
/ \ E2E-tester (Få)
/____\
/ \ Integrationstester (Några)
/________\
/ \ Enhetstester (Många)
/____________\
Enhetstester bildar grunden - de är många, snabba och ger snabb feedback.
Jämförelse av Python-testramverk
unittest: Det inbyggda ramverket
Pythons standardbibliotek innehåller unittest, inspirerat av JUnit:
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
"""Kör före varje test"""
self.calc = Calculator()
def tearDown(self):
"""Kör efter varje 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()
Fördelar:
- Inbyggt, ingen installation behövs
- Bra för utvecklare som är bekanta med xUnit-ramverk
- Företagsvänligt, väl etablerat
Nackdelar:
- Omständlig syntax med boilerplate
- Kräver klasser för testorganisation
- Mindre flexibel fixture-hantering
pytest: Det moderna valet
pytest är det mest populära tredjeparts-testramverket:
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
Fördelar:
- Enkel, Pythonisk syntax
- Kraftfullt fixturesystem
- Utmärkt plugin-ecosystem
- Bättre felrapportering
- Parametriserad testning inbyggd
Nackdelar:
- Kräver installation
- Mindre bekant för utvecklare från andra språk
Installation:
pip install pytest pytest-cov pytest-mock
Att skriva dina första enhetstester
Låt oss bygga ett enkelt exempel från grunden med hjälp av Test-Driven Development (TDD). Om du är ny på Python eller behöver en snabb referens för syntax och språkfunktioner, kolla in vår Python Cheatsheet för en omfattande översikt av Python-grunderna.
Exempel: Strängverktygsfunktioner
Steg 1: Skriv testen först (Röd)
Skapa 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
Steg 2: Skriv minimal kod för att klara testen (Grön)
Skapa string_utils.py:
def reverse_string(s: str) -> str:
"""Vänd på en sträng"""
return s[::-1]
def is_palindrome(s: str) -> bool:
"""Kolla om strängen är palindrom (ignorerar versaler och mellanslag)"""
cleaned = ''.join(s.lower().split())
return cleaned == cleaned[::-1]
def count_vowels(s: str) -> int:
"""Räkna vokaler i sträng"""
return sum(1 for char in s.lower() if char in 'aeiou')
Steg 3: Kör testerna
pytest test_string_utils.py -v
Utdata:
test_string_utils.py::test_reverse_string PASSED
test_string_utils.py::test_is_palindrome PASSED
test_string_utils.py::test_count_vowels PASSED
Avancerade testtekniker
Använda fixtures för testuppsättning
Fixtures ger återanvändbar testuppsättning och nedrivning:
import pytest
from database import Database
@pytest.fixture
def db():
"""Skapa en testdatabas"""
database = Database(":memory:")
database.create_tables()
yield database # Ge till test
database.close() # Rensa efter test
@pytest.fixture
def sample_users(db):
"""Lägg till provanvändare i databasen"""
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-omfattningar
Styr fixture-livstid med omfattningar:
@pytest.fixture(scope="function") # Standard: kör för varje test
def func_fixture():
return create_resource()
@pytest.fixture(scope="class") # En gång per testklass
def class_fixture():
return create_resource()
@pytest.fixture(scope="module") # En gång per modul
def module_fixture():
return create_resource()
@pytest.fixture(scope="session") # En gång per testsession
def session_fixture():
return create_expensive_resource()
Att mocka externa beroenden
Använd mockning för att isolera kod från externa beroenden:
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 med mock
def test_get_temperature():
service = WeatherService()
# Mocka requests.get-funktionen
with patch('requests.get') as mock_get:
# Konfigurera mock-svar
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
# Verifiera anropet
mock_get.assert_called_once_with("https://api.weather.com/Boston")
Använda pytest-mock-plugin
pytest-mock ger en renare syntax:
def test_get_temperature(mocker):
service = WeatherService()
# Mocka med 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
Parametriserad testning
Testa flera scenarier effektivt:
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
Testning av undantag och felhantering
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Kan inte dela med noll")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Kan inte dela med noll"):
divide(10, 0)
def test_divide_by_zero_with_message():
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "noll" in str(exc_info.value).lower()
# Testning att inget undantag kastas
def test_divide_success():
result = divide(10, 2)
assert result == 5.0
Testning av asynkron kod
Testning av asynkron kod är avgörande för moderna Python-applikationer, särskilt när man arbetar med API:er, databaser eller AI-tjänster. Här är hur man testar asynkrona funktioner:
import pytest
import asyncio
async def fetch_data(url):
"""Asynkron funktion för att hämta data"""
await asyncio.sleep(0.1) # Simulera API-anrop
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):
# Mocka den asynkrona funktionen
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"
För praktiska exempel på testning av asynkron kod med AI-tjänster, se vår guide om integrering av Ollama med Python, som inkluderar teststrategier för LLM-interaktioner.
Kodtäckning
Mät hur mycket av din kod som testas:
Användande av pytest-cov
# Kör tester med täckning
pytest --cov=myproject tests/
# Generera HTML-rapportering
pytest --cov=myproject --cov-report=html tests/
# Visa saknade rader
pytest --cov=myproject --cov-report=term-missing tests/
Täckningskonfiguration
Skapa .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
Täckningsbästa praxis
- Mål på 80%+ täckning för kritiska kodvägar
- Fokusera inte på 100% - koncentrera dig på meningsfulla tester
- Testa gränssituationer inte bara lyckade scenarier
- Exkludera boilerplate från täckningsrapporter
- Använd täckning som vägledning inte som mål
Testorganisation och projektstruktur
Rekommenderad struktur
myproject/
├── myproject/
│ ├── __init__.py
│ ├── module1.py
│ ├── module2.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Delade fixar
│ ├── test_module1.py
│ ├── test_module2.py
│ ├── test_utils.py
│ └── integration/
│ ├── __init__.py
│ └── test_integration.py
├── pytest.ini
├── requirements.txt
└── requirements-dev.txt
conftest.py för delade fixar
# tests/conftest.py
import pytest
from myproject.database import Database
@pytest.fixture(scope="session")
def test_db():
"""Session-bredd testdatabas"""
db = Database(":memory:")
db.create_schema()
yield db
db.close()
@pytest.fixture
def clean_db(test_db):
"""Rensad databas för varje test"""
test_db.clear_all_tables()
return test_db
pytest.ini Konfiguration
[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: markerar tester som långsamma
integration: markerar tester som integrationstester
unit: markerar tester som enhetstester
Bästa praxis för enhetstestning
1. Följ AAA-mallen
Arrangera-Agera-Assertera gör testerna tydliga:
def test_anvandar_skapande():
# Arrangera
anvandar_namn = "john_doe"
epost = "john@example.com"
# Agera
anvandar = User(anvandar_namn, epost)
# Assertera
assert anvandar.anvandar_namn == anvandar_namn
assert anvandar.epost == epost
assert anvandar.is_active == True
2. En assertion per test (Riktlinje, inte regel)
# Bra: Fokuserad test
def test_anvandar_namn():
anvandar = User("john_doe", "john@example.com")
assert anvandar.anvandar_namn == "john_doe"
def test_anvandar_epost():
anvandar = User("john_doe", "john@example.com")
assert anvandar.epost == "john@example.com"
# Även acceptabelt: Relaterade assertioner
def test_anvandar_skapande():
anvandar = User("john_doe", "john@example.com")
assert anvandar.anvandar_namn == "john_doe"
assert anvandar.epost == "john@example.com"
assert isinstance(anvandar.created_at, datetime)
3. Använd beskrivande testnamn
# Dåligt
def test_anvandar():
pass
# Bra
def test_anvandar_skapande_med_giltig_data():
pass
def test_anvandar_skapande_misslyckas_med_ogiltig_epost():
pass
def test_anvandar_losenord_ar_haschat_efter_installation():
pass
4. Testa gränssituationer och gränser
def test_alder_validering():
# Giltiga fall
assert validate_age(0) == True
assert validate_age(18) == True
assert validate_age(120) == True
# Gränssituationer
assert validate_age(-1) == False
assert validate_age(121) == False
# Extremfall
with pytest.raises(TypeError):
validate_age("18")
with pytest.raises(TypeError):
validate_age(None)
5. Håll testerna oberoende
# Dåligt: Testerna beror på ordning
counter = 0
def test_incrementera():
global counter
counter += 1
assert counter == 1
def test_incrementera_igen(): # Misslyckas om den körs ensam
global counter
counter += 1
assert counter == 2
# Bra: Testerna är oberoende
def test_incrementera():
counter = Counter()
counter.incrementera()
assert counter.value == 1
def test_incrementera_flera_ganger():
counter = Counter()
counter.incrementera()
counter.incrementera()
assert counter.value == 2
6. Testa inte implementeringsdetaljer
# Dåligt: Testa implementering
def test_sort_anvander_quicksort():
sorterare = Sorter()
assert sorterare.algorithm == "quicksort"
# Bra: Testa beteende
def test_sort_returnerar_sorterad_lista():
sorterare = Sorter()
resultat = sorterare.sort([3, 1, 2])
assert resultat == [1, 2, 3]
7. Testa verkliga användningsfall
När du testar bibliotek som bearbetar eller omvandlar data, fokusera på verkliga scenarier. Till exempel, om du arbetar med webbskrapning eller innehållsomvandling, kolla vårt guide om konvertera HTML till Markdown med Python, som inkluderar teststrategier och bänkmärkningar för olika omvandlingsbibliotek.
Integration med kontinuerlig integration
GitHub Actions Exempel
# .github/workflows/tests.yml
name: Tester
on: [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: Konfigurera Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Installera beroenden
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Köra tester
run: |
pytest --cov=myproject --cov-report=xml
- name: Ladda upp täckning
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Testning av serverlösa funktioner
När du testar AWS Lambda-funktioner eller serverlösa applikationer, överväg integrationsteststrategier tillsammans med enhetstester. Vår guide om bygga en dubbelmodig AWS Lambda med Python och Terraform täcker testmetoder för serverlösa Python-applikationer, inklusive hur man testar Lambda-handlare, SQS-konsumenter och API Gateway-integrationer.
Checklista för testningens bästa praxis
- Skriv tester före eller tillsammans med kod (TDD)
- Håll testerna snabba (< 1 sekund per test)
- Gör testerna oberoende och isolerade
- Använd beskrivande testnamn
- Följ AAA-mallen (Arrangera-Agera-Assertera)
- Testa gränssituationer och felvillkor
- Mocka externa beroenden
- Sträva efter 80%+ kodtäckning
- Köra tester i CI/CD-pipeline
- Granska och refaktorera tester regelbundet
- Dokumentera komplexa testscenarier
- Använd fixar för gemensam konfiguration
- Parametrisera liknande tester
- Håll testerna enkla och läsbara
Vanliga testmönster
Testning av klasser
class TestAnvandar:
@pytest.fixture
def anvandar(self):
return User("john_doe", "john@example.com")
def test_anvandar_namn(self, anvandar):
assert anvandar.anvandar_namn == "john_doe"
def test_epost(self, anvandar):
assert anvandar.epost == "john@example.com"
def test_fullt_namn(self, anvandar):
anvandar.first_name = "John"
anvandar.last_name = "Doe"
assert anvandar.full_name() == "John Doe"
Testning med tillfälliga filer
import pytest
from pathlib import Path
@pytest.fixture
def temp_fil(tmp_path):
"""Skapa en tillfällig fil"""
fil_sokvag = tmp_path / "test_file.txt"
fil_sokvag.write_text("test innehåll")
return fil_sokvag
def test_las_fil(temp_fil):
innehall = temp_fil.read_text()
assert innehall == "test innehåll"
Testning av filgenerering
När du testar kod som genererar filer (som PDF:er, bilder eller dokument), använd tillfälliga kataloger och verifiera filegenskaper:
@pytest.fixture
def temp_utdata_katalog(tmp_path):
"""Leverera en tillfällig utdatakatalog"""
utdata_katalog = tmp_path / "utdata"
utdata_katalog.mkdir()
return utdata_katalog
def test_pdf_generering(temp_utdata_katalog):
pdf_sokvag = temp_utdata_katalog / "utdata.pdf"
generate_pdf(pdf_sokvag, content="Test")
assert pdf_sokvag.exists()
assert pdf_sokvag.stat().st_size > 0
För omfattande exempel på testning av PDF-generering, se vårt guide om generera PDF i Python, som täcker teststrategier för olika PDF-bibliotek.
Testning med Monkeypatch
def test_miljo_variabel(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key_123")
assert os.getenv("API_KEY") == "test_key_123"
def test_modul_attribut(monkeypatch):
monkeypatch.setattr("modul.KONSTANT", 42)
assert modul.KONSTANT == 42
Användbara länkar och resurser
- pytest Dokumentation
- unittest Dokumentation
- pytest-cov Plugin
- pytest-mock Plugin
- unittest.mock Dokumentation
- Test Driven Development by Example - Kent Beck
- Python Testing with pytest - Brian Okken
Relaterade resurser
Python Grundläggande och Bästa Praktiker
- Python Cheatsheet - Komplett referens för Python-syntax, datatyper och vanliga mönster
Testning av Specifika Python-Användningsfall
- Integrera Ollama med Python - Teststrategier för LLM-integrationer och asynkrona AI-serviceanrop
- Konvertera HTML till Markdown med Python - Testmetoder för webbskrapning och innehållsomvandlingsbibliotek
- Generera PDF i Python - Teststrategier för PDF-generering och filutdata
Serverlös och molntestning
- Bygga en dubbelmodig AWS Lambda med Python och Terraform - Testning av Lambda-funktioner, SQS-konsumenter och API Gateway-integrationer
Enhetstestning är en grundläggande färdighet för Python-utvecklare. Oavsett om du väljer unittest eller pytest, är nyckeln att skriva tester konsekvent, hålla dem underhållbara och integrera dem i din utvecklingsprocess. Börja med enkla tester, adoptera gradvis avancerade tekniker som mockning och fixar, och använd täckningsverktyg för att identifiera otestad kod.