In Pytest, dem beliebtesten Python-Testframework aller, ist ein Fixture ein wiederverwendbarer Codeabschnitt, der etwas anordnet, bevor der Test beginnt, und nach dem Beenden aufräumt. Zum Beispiel eine temporäre Datei oder einen temporären Ordner, eine Setup-Umgebung, das Starten eines Webservers usw. In diesem Beitrag schauen wir uns an, wie man ein Pytest-Fixture erstellt, das eine Testdatenbank (leer oder mit bekanntem Status) erstellt, die abgerufen wird bereinigt, sodass jeder Test auf einer völlig sauberen Datenbank ausgeführt werden kann.
Die Ziele
Wir werden mit Psycopg 3 ein Pytest-Gerät erstellen, um die Testdatenbank vorzubereiten und zu bereinigen. Da eine leere Datenbank zum Testen selten hilfreich ist, werden wir optional Yoyo-Migrationen anwenden (zum Zeitpunkt des Schreibens ist die Website nicht verfügbar, gehen Sie zum Snapshot von archive.org), um sie aufzufüllen.
Die Anforderungen für das Pytest-Fixture namens test_db, das in diesem Blogbeitrag erstellt wurde, sind also:
- Testdatenbank löschen, falls vor dem Test vorhanden
- erstellen Sie eine leere Datenbank vor dem Test
optional - Migrationen anwenden oder Testdaten erstellen vor dem Test
- Stellen Sie eine Verbindung zur Testdatenbank her zum Test
- Testdatenbank löschen nach dem Test (auch im Fehlerfall)
Jede Testmethode, die dies anfordert, indem sie ein Testmethodenargument auflistet:
def test_create_admin_table(test_db):
...
def test_create_admin_table(test_db):
...
Erhält eine reguläre Psycopg-Verbindungsinstanz, die mit der Test-DB verbunden ist. Der Test kann alles tun, was er benötigt, wie bei der allgemeinen Verwendung von Psycopg, z. B.:
def test_create_admin_table(test_db):
# Öffnen Sie einen Cursor, um Datenbankoperationen auszuführen
cur = test_db.cursor()
# Übergeben Sie Daten, um Platzhalter für eine Abfrage zu füllen und Psycopg ausführen zu lassen
# die richtige Konvertierung (keine SQL-Injections!)
cur.execute(
„INSERT INTO test (num, data) VALUES (%s, %s)“,
(100, „abc'def“))
# Fragen Sie die Datenbank ab und erhalten Sie Daten als Python-Objekte.
cur.execute("SELECT * FROM test")
cur.fetchone()
# wird zurückgeben (1, 100, „abc'def“)
# Sie können „cur.fetchmany()“ und „cur.fetchall()“ verwenden, um eine Liste zurückzugeben
Anzahl mehrerer Datensätze oder sogar Iteration am Cursor
für Aufnahme in cur:
drucken (aufzeichnen)
def test_create_admin_table(test_db):
...
Motivation & Alternativen
Es sieht so aus, als gäbe es einige Pytest-Plugins, die PostgreSQL-Fixtures für Tests versprechen, die auf Datenbanken basieren. Sie könnten für Sie gut funktionieren.
Ich habe pytest-postgresql ausprobiert, was dasselbe verspricht. Ich habe es versucht, bevor ich mein eigenes Fixture geschrieben habe, aber ich konnte es nicht für mich zum Laufen bringen. Vielleicht, weil ihre Dokumente für mich sehr verwirrend waren.
Noch eins, pytest-dbt-postgres, ich habe es überhaupt nicht versucht.
Layout der Projektdatei
In klassischen Python-Projekten befinden sich die Quellen in src/ und Tests in Tests/:
├── src
│ └── tuvok
│ ├── __init__.py
│ └── Verkauf
│ └── new_user.py
├── Tests
│ ├── conftest.py
│ └── Verkauf
│ └── test_new_user.py
├── Anforderungen.txt
└── yoyo.ini
def test_create_admin_table(test_db):
...
Wenn Sie eine Migrationsbibliothek wie das fantastische Yoyo verwenden, befinden sich Migrationsskripte wahrscheinlich in migrations/:
├── Migrationen
├── 20240816_01_Yn3Ca-sales-user-user-add-last-run-table.py
├── ...
def test_create_admin_table(test_db):
...
Konfiguration
Unser Test-DB-Gerät benötigt nur eine sehr kleine Konfiguration:
- Verbindungs-URL – (ohne Datenbank)
- Name der Testdatenbank – wird für jeden Test neu erstellt
(optional) - Migrationsordner – Migrationsskripte, die für jeden Test angewendet werden sollen
Pytest verfügt über einen natürlichen Ort conftest.py zum Teilen von Fixtures über mehrere Dateien hinweg. Dort wird auch die Fixture-Konfiguration abgelegt:
# Ohne DB-Namen!
TEST_DB_URL = "postgresql://localhost"
TEST_DB_NAME = "test_tuvok"
TEST_DB_MIGRATIONS_DIR = str(Path(__file__, "../../migrations").resolve())
def test_create_admin_table(test_db):
...
Sie können diese Werte über die Umgebungsvariable oder was auch immer für Ihren Fall geeignet ist, festlegen.
Erstellen Sie test_db-Fixture
Mit Kenntnissen von
PostgreSQL und der Psycopg-Bibliothek schreiben Sie das Fixture in conftest.py:
@pytest.fixture
def test_db():
# autocommit=True, keine Transaktion starten, da DATENBANK ERSTELLEN/ENTFERNEN
# kann nicht in einem Transaktionsblock ausgeführt werden.
mit psycopg.connect(TEST_DB_URL, autocommit=True) als conn:
cur = conn.cursor()
# Test-DB erstellen, vorher löschen
cur.execute(f'DROP DATABASE IF EXISTS "{TEST_DB_NAME}" WITH (FORCE)')
cur.execute(f'CREATE DATABASE "{TEST_DB_NAME}"')
# Gibt eine (neue) Verbindung zur gerade erstellten Test-DB zurück
# Leider können Sie die Datenbank für eine bestehende Psycopg-Verbindung nicht direkt ändern. Sobald eine Verbindung zu einer bestimmten Datenbank hergestellt wurde, wird sie an diese Datenbank gebunden.
mit psycopg.connect(TEST_DB_URL, dbname=TEST_DB_NAME) als conn:
Ertragsverb
cur.execute(f'DROP DATABASE IF EXISTS "{TEST_DB_NAME}" WITH (FORCE)')
def test_create_admin_table(test_db):
...
Erstellen Sie eine Migrationseinrichtung
In unserem Fall verwenden wir
Yoyo-Migrationen. Schreiben Sie Apply-Migrationen als ein weiteres Fixture mit dem Namen yoyo:
@pytest.fixture
def yoyo():
# Yoyo erwartet `driver://user:pass@host:port/database_name?param=value`.
# In der übergebenen URL müssen wir
URL = (
urlparse(TEST_DB_URL)
.
# 1) Ändern Sie den Treiber (Schema-Teil) mit „postgresql psycopg“, um ihn zu verwenden
# psycopg 3 (nicht 2, was „postgresql psycopg2“ ist)
_replace(scheme="postgresql psycopg")
.
# 2) Datenbank in Testdatenbank ändern (in der Migrationen angewendet werden)
_replace(path=TEST_DB_NAME)
.geturl()
)
backend = get_backend(url)
migrations = read_migrations(TEST_DB_MIGRATIONS_DIR)
wenn len(migrations) == 0:
raise ValueError(f"Keine Yoyo-Migrationen gefunden in '{TEST_DB_MIGRATIONS_DIR}'")
mit backend.lock():
backend.apply_migrations(backend.to_apply(migrations))
def test_create_admin_table(test_db):
...
Wenn Sie
Migrationen auf jede Testdatenbank anwenden möchten, benötigen Sie ein Yoyo-Fixture für test_db-Fixture:
@pytest.fixture
def test_db(yoyo):
...
def test_create_admin_table(test_db):
...
Um
die Migration nur auf einige Tests anzuwenden, erfordern Sie Yoyo einzeln:
def test_create_admin_table(test_db, yoyo):
...
def test_create_admin_table(test_db):
...
Abschluss
Das Erstellen einer eigenen Vorrichtung, um Ihren Tests eine saubere Datenbank zu bieten, war für mich eine lohnende Erfahrung, die es mir ermöglichte, tiefer in Pytest und Postgres einzutauchen.
Ich hoffe, dieser Artikel hat Ihnen bei Ihrer eigenen Datenbanktestsuite geholfen. Hinterlassen Sie mir Ihre Frage gerne in den Kommentaren und viel Spaß beim Codieren!