Enhetstestning i Python: Komplett guide med exempel

Python-testning med pytest, TDD, mockning och täckning

Sidinnehåll

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.

Python Unit Testing

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

  1. Mål på 80%+ täckning för kritiska kodvägar
  2. Fokusera inte på 100% - koncentrera dig på meningsfulla tester
  3. Testa gränssituationer inte bara lyckade scenarier
  4. Exkludera boilerplate från täckningsrapporter
  5. 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

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

Serverlös och molntestning


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.