Fingest: Eein selbstentwickeltes pytest Plugin für bessere Test-Fixtures
Entdecken Sie fingest, ein selbstentwickeltes pytest Plugin für intelligente Dateibasierte Fixture-Verwaltung.
Einleitung
Als Entwickler bin ich täglich mit der Herausforderung konfrontiert, umfassende und wartbare Tests zu schreiben. Besonders bei datengetriebenen Tests stößt man schnell an die Grenzen traditioneller Ansätze. Aus diesem Grund habe ich Fingest entwickelt - ein pytest Plugin, das die Erstellung und Verwaltung von datengetriebenen Fixtures erheblich vereinfacht.
Was ist Fingest?
Fingest ist ein mächtiges pytest Plugin, das datengetriebene Tests ermöglicht, indem es automatisch Fixtures aus externen Datendateien erstellt. Der Name steht für "Fixture Ingest" und beschreibt perfekt, was das Plugin leistet: Es "verschlingt" Daten aus verschiedenen Quellen und macht sie als typisierte pytest-Fixtures verfügbar.
Die Motivation hinter Fingest
In meiner täglichen Arbeit als Python-Entwickler bin ich immer wieder auf folgende Probleme gestoßen:
🔄 Repetitive Fixture-Erstellung
# Traditioneller Ansatz - viel Boilerplate-Code
@pytest.fixture
def user_data():
with open("tests/data/users.json") as f:
return json.load(f)
@pytest.fixture
def product_data():
with open("tests/data/products.csv") as f:
reader = csv.DictReader(f)
return list(reader)
📁 Unstrukturierte Testdaten
- Testdaten verstreut über verschiedene Dateien
- Keine einheitliche Zugriffsmethoden
- Schwierige Wartung und Aktualisierung
🎯 Fehlende Typisierung
- Keine IDE-Unterstützung für Datenstrukturen
- Laufzeitfehler durch falsche Annahmen über Datenformate
- Schwierige Refaktorierung
Hauptfunktionen von Fingest
🚀 Automatische Fixture-Registrierung
Fingest erkennt automatisch Datendateien und erstellt entsprechende Fixtures:
from fingest import data_fixture, JSONFixture
@data_fixture("users.json", description="Test user data")
class user_data(JSONFixture):
"""Fixture für Benutzertestdaten."""
pass
📁 Mehrere Datenformate unterstützt
- JSON: Für strukturierte API-Daten und Konfigurationen
- CSV: Für tabellarische Daten und Berichte
- XML: Für Konfigurationsdateien und Legacy-Systeme
- Erweiterbar: Eigene Datenloader für beliebige Formate
🎯 Typisierte Basis-Klassen
Jeder Datentyp erhält spezialisierte Methoden:
# JSON-spezifische Operationen
def test_json_operations(user_data):
assert "users" in user_data.keys()
user_count = user_data.get("user_count", 0)
first_user = user_data["users"][0]
# CSV-spezifische Operationen
def test_csv_operations(product_data):
columns = product_data.columns
prices = product_data.get_column("price")
expensive_items = product_data.filter_rows(price="999.99")
# XML-spezifische Operationen
def test_xml_operations(config_data):
timeout = config_data.find("timeout")
features = config_data.findall("features/feature")
enabled_features = config_data.xpath("//feature[@enabled='true']")
Installation und Setup
Installation
pip install fingest
Konfiguration in pytest.ini
[pytest]
fingest_fixture_path = tests/data # Pfad zu Ihren Datendateien
Projektstruktur
my_project/
├── tests/
│ ├── data/
│ │ ├── users.json
│ │ ├── products.csv
│ │ └── config.xml
│ ├── conftest.py
│ └── test_*.py
└── pytest.ini
Praktische Beispiele
JSON-Fixtures für API-Tests
# tests/data/api_response.json
{
"users": [
{"id": 1, "name": "Alice", "email": "[email protected]", "active": true},
{"id": 2, "name": "Bob", "email": "[email protected]", "active": false}
],
"total_count": 2,
"page": 1
}
# conftest.py
@data_fixture("api_response.json", description="API response data")
class api_data(JSONFixture):
pass
# test_api.py
def test_user_count(api_data):
assert len(api_data["users"]) == 2
assert api_data.get("total_count") == 2
def test_active_users(api_data):
active_users = [u for u in api_data["users"] if u["active"]]
assert len(active_users) == 1
assert active_users[0]["name"] == "Alice"
CSV-Fixtures für Datenanalyse
# tests/data/sales_data.csv
product_id,product_name,price,category,sales_count
1,Laptop,999.99,Electronics,150
2,Mouse,29.99,Electronics,300
3,Keyboard,79.99,Electronics,200
# conftest.py
@data_fixture("sales_data.csv", description="Sales analytics data")
class sales_data(CSVFixture):
pass
# test_analytics.py
def test_product_categories(sales_data):
categories = set(sales_data.get_column("category"))
assert "Electronics" in categories
def test_price_analysis(sales_data):
prices = [float(p) for p in sales_data.get_column("price")]
assert max(prices) == 999.99
assert min(prices) == 29.99
def test_high_value_products(sales_data):
expensive = sales_data.filter_rows(price="999.99")
assert len(expensive) == 1
assert expensive[0]["product_name"] == "Laptop"
XML-Fixtures für Konfigurationstests
# tests/data/app_config.xml
<?xml version="1.0"?>
<config>
<database>
<host>localhost</host>
<port>5432</port>
<timeout>30</timeout>
</database>
<features>
<feature name="auth" enabled="true"/>
<feature name="cache" enabled="false"/>
<feature name="logging" enabled="true"/>
</features>
</config>
# conftest.py
@data_fixture("app_config.xml", description="Application configuration")
class app_config(XMLFixture):
pass
# test_config.py
def test_database_config(app_config):
host = app_config.get_text("database/host")
port = int(app_config.get_text("database/port"))
assert host == "localhost"
assert port == 5432
def test_enabled_features(app_config):
enabled_features = app_config.xpath("//feature[@enabled='true']")
feature_names = [f.get("name") for f in enabled_features]
assert "auth" in feature_names
assert "logging" in feature_names
assert "cache" not in feature_names
Erweiterte Funktionen
Funktionsbasierte Fixtures
Für komplexe Datenverarbeitung:
@data_fixture("raw_users.json", description="Processed user data")
def processed_users(data):
"""Transformiert rohe Benutzerdaten."""
return [
{
"id": user["id"],
"display_name": f"{user['first_name']} {user['last_name']}",
"email": user["email"].lower(),
"is_admin": user.get("role") == "admin"
}
for user in data["users"]
]
def test_processed_data(processed_users):
assert processed_users[0]["display_name"] == "John Doe"
assert "@" in processed_users[0]["email"]
assert isinstance(processed_users[0]["is_admin"], bool)
Eigene Datenloader
Unterstützung für beliebige Dateiformate:
from fingest import register_loader
import yaml
import toml
# YAML-Support
def yaml_loader(path):
with open(path, 'r') as f:
return yaml.safe_load(f)
register_loader("yaml", yaml_loader)
@data_fixture("config.yaml", description="YAML configuration")
class yaml_config(BaseFixture):
pass
# TOML-Support
def toml_loader(path):
with open(path, 'r') as f:
return toml.load(f)
register_loader("toml", toml_loader)
Umgebungsspezifische Daten
import os
from fingest import data_fixture
env = os.getenv("TEST_ENV", "dev")
@data_fixture(f"config_{env}.json", description=f"Config for {env}")
class environment_config(JSONFixture):
pass
# Lädt automatisch:
# - config_dev.json (Entwicklung)
# - config_staging.json (Staging)
# - config_prod.json (Produktion)
Technische Details
Plugin-Architektur
Fingest nutzt pytest's Hook-System für nahtlose Integration:
# Automatische Registrierung über entry_points
[tool.poetry.plugins."pytest11"]
fingest = "fingest.plugin"
Datenloader-System
Erweiterbare Architektur für neue Formate:
# Eingebaute Loader
BUILTIN_LOADERS = {
"json": json_loader,
"csv": csv_loader,
"xml": xml_loader
}
# Registrierung eigener Loader
def register_loader(extension: str, loader_func: Callable):
"""Registriert einen benutzerdefinierten Datenloader."""
CUSTOM_LOADERS[extension] = loader_func
Typisierung und IDE-Support
Vollständige Typisierung für bessere Entwicklererfahrung:
from typing import Dict, List, Any, Optional
from pathlib import Path
class BaseFixture:
def __init__(self, data: Any) -> None: ...
def __len__(self) -> int: ...
def __bool__(self) -> bool: ...
class JSONFixture(BaseFixture):
def get(self, key: str, default: Any = None) -> Any: ...
def keys(self) -> Dict.KeysView: ...
def values(self) -> Dict.ValuesView: ...
Qualitätssicherung
Umfassende Tests
- 84 Tests mit 100% Erfolgsrate
- Vollständige Abdeckung aller Funktionen
- Integration Tests für alle unterstützten Formate
- Performance Tests für große Datensätze
Code-Qualität
# Automatische Code-Formatierung
black src/ tests/
isort src/ tests/
# Linting und Typisierung
flake8 src/ tests/
mypy src/
# Test-Ausführung mit Coverage
pytest --cov=fingest --cov-report=html
CI/CD Pipeline
# .github/workflows/test.yml
- name: Run tests
run: |
pytest --cov=fingest
pytest --cov=fingest --cov-report=xml
- name: Code quality
run: |
black --check src/ tests/
isort --check-only src/ tests/
flake8 src/ tests/
mypy src/
Vergleich mit traditionellen Ansätzen
Vorher (Traditionell)
import json
import csv
import xml.etree.ElementTree as ET
@pytest.fixture
def user_data():
with open("tests/data/users.json") as f:
return json.load(f)
@pytest.fixture
def product_data():
with open("tests/data/products.csv") as f:
reader = csv.DictReader(f)
return list(reader)
@pytest.fixture
def config_data():
tree = ET.parse("tests/data/config.xml")
return tree.getroot()
def test_users(user_data):
# Keine IDE-Unterstützung
# Keine typisierte Methoden
assert len(user_data["users"]) == 2
Nachher (Mit Fingest)
from fingest import data_fixture, JSONFixture, CSVFixture, XMLFixture
@data_fixture("users.json")
class user_data(JSONFixture): pass
@data_fixture("products.csv")
class product_data(CSVFixture): pass
@data_fixture("config.xml")
class config_data(XMLFixture): pass
def test_users(user_data):
# Vollständige IDE-Unterstützung
# Typisierte Methoden verfügbar
assert len(user_data["users"]) == 2
assert user_data.get("total_count", 0) > 0
Performance und Skalierung
Lazy Loading
# Daten werden nur bei Bedarf geladen
@data_fixture("large_dataset.json")
class large_data(JSONFixture):
pass
# Wird nur geladen, wenn der Test tatsächlich ausgeführt wird
def test_large_data(large_data):
assert len(large_data) > 1000
Caching
# Automatisches Caching für bessere Performance
# Datei wird nur einmal pro Test-Session geladen
@data_fixture("shared_config.json", scope="session")
class shared_config(JSONFixture):
pass
Zukunftspläne
Geplante Features
- Datenvalidierung: Schema-Validierung für JSON/XML
- Daten-Mocking: Automatische Generierung von Testdaten
- Database-Support: Direkte Verbindung zu Datenbanken
- Cloud-Integration: Support für S3, GCS, Azure Blob Storage
- Performance-Optimierung: Paralleles Laden großer Datensätze
Community-Beiträge
# Beispiel für geplante Database-Integration
@data_fixture("SELECT * FROM users", loader=sql_loader)
class db_users(SQLFixture):
pass
# Beispiel für Cloud-Integration
@data_fixture("s3://bucket/data.json", loader=s3_loader)
class cloud_data(JSONFixture):
pass
Fazit
Fingest revolutioniert die Art, wie wir datengetriebene Tests in Python schreiben. Durch die Kombination aus:
- Automatischer Fixture-Registrierung
- Typisierten Basis-Klassen
- Erweiterbarer Architektur
- Umfassender Dokumentation
wird das Schreiben und Warten von Tests erheblich vereinfacht. Das Plugin ist bereits produktionsreif und wird aktiv in mehreren Projekten eingesetzt.
Weiterführende Ressourcen
- GitHub Repository: https://github.com/0x68/fingest
- PyPI Package: https://pypi.org/project/fingest/
- Dokumentation: README auf GitHub
- Issue Tracker: GitHub Issues
Installation und erste Schritte
# Installation
pip install fingest
# Beispiel-Projekt klonen
git clone https://github.com/0x68/fingest.git
cd fingest/examples
# Tests ausführen
pytest -v
Haben Sie Erfahrungen mit datengetriebenen Tests oder Interesse an Fingest? Ich freue mich über Feedback, Beiträge und Diskussionen! Kontaktieren Sie mich gerne über GitHub oder direkt per E-Mail.
Entwickelt mit ❤️ von Tim Fiedler