Maison  >  Article  >  développement back-end  >  Pytest et PostgreSQL : nouvelle base de données pour chaque test (partie II)

Pytest et PostgreSQL : nouvelle base de données pour chaque test (partie II)

WBOY
WBOYoriginal
2024-09-03 16:09:19835parcourir

Pytest and PostgreSQL: Fresh database for every test (part II)

Dans l'article précédent, nous avons créé le luminaire Pytest qui créera/supprimera la base de données Postgres avant/après la méthode de test. Dans cette partie, je souhaite améliorer le luminaire pour qu'il soit plus flexible et configurable à l'aide des luminaires d'usine Pytest.

Limites du luminaire statique

Par exemple, si vous avez plus d'une base de données à simuler dans le test

def test_create_user(test_db1, test_db2):
    ...

vous devez créer presque deux luminaires identiques :

TEST_DB_URL = "postgresql://localhost"
TEST_DB1_NAME = "test_foo"
TEST_DB2_NAME = "test_bar"

@pytest.fixture
def test_db1():
    with psycopg.connect(TEST_DB_URL, autocommit=True) as conn:
        cur = conn.cursor()

        cur.execute(f'DROP DATABASE IF EXISTS "{TEST_DB1_NAME}" WITH (FORCE)')
        cur.execute(f'CREATE DATABASE "{TEST_DB1_NAME}"')

        with psycopg.connect(TEST_DB_URL, dbname=TEST_DB1_NAME) as conn:
            yield conn

        cur.execute(f'DROP DATABASE IF EXISTS "{TEST_DB1_NAME}" WITH (FORCE)')

@pytest.fixture
def test_db2():
    with psycopg.connect(TEST_DB_URL, autocommit=True) as conn:
        cur = conn.cursor()

        cur.execute(f'DROP DATABASE IF EXISTS "{TEST_DB2_NAME}" WITH (FORCE)')
        cur.execute(f'CREATE DATABASE "{TEST_DB2_NAME}"')

        with psycopg.connect(TEST_DB_URL, dbname=TEST_DB2_NAME) as conn:
            yield conn

        cur.execute(f'DROP DATABASE IF EXISTS "{TEST_DB2_NAME}" WITH (FORCE)')

Usines de luminaires Pytest

Les luminaires "statiques" sont un peu limitatifs ici. Lorsque vous en avez besoin à peu près de la même manière avec juste une légère différence, vous devez dupliquer un code. Espérons que le Pytest ait un concept d'usines comme accessoires.

Un luminaire d'usine est un luminaire qui renvoie un autre luminaire. Parce que, comme toute usine, c'est une fonction, elle peut accepter des arguments pour personnaliser les luminaires renvoyés. Par convention, vous pouvez les préfixer avec make_*, comme make_test_db.

Luminaires spécialisés

Le seul argument de notre fabrique d'appareils make_test_db sera un nom de base de données de test à créer/supprimer.

Alors, créons deux luminaires "spécialisés" basés sur le luminaire d'usine make_test_db.

L'utilisation ressemblera à :

@pytest.fixture
def test_db_foo(make_test_db):
    yield from make_test_db("test_foo")

@pytest.fixture
def test_db_bar(make_test_db):
    yield from make_test_db("test_bar")

Sidenote : rendement de

Avez-vous remarqué le rendement de ? Il existe une différence clé entre le rendement et le rendement selon la manière dont ils gèrent le flux de données et le contrôle au sein des générateurs.

En Python, le rendement et le rendement de sont utilisés dans les fonctions du générateur pour produire une séquence de valeurs, mais

  • rendement est utilisé pour suspendre l'exécution d'une fonction génératrice et renvoyer une valeur unique à l'appelant.
  • tandis que le rendement de est utilisé pour déléguer la génération des valeurs à un autre générateur. Il « aplatit » essentiellement le générateur imbriqué, en transmettant ses valeurs générées directement à l'appelant du générateur externe.

C'est-à-dire nous ne voulons pas "céder" d'un luminaire spécialisé mais d'une usine de luminaires. Par conséquent, le rendement de est requis ici.

Usine de luminaires pour créer/supprimer la base de données

Les modifications requises dans notre base de données de création/suppression de luminaires d'origine ne sont en fait presque aucune, à l'exception de l'encapsulation du code dans la fonction interne.

@pytest.fixture
def make_test_db():
    def _(test_db_name: str):
        with psycopg.connect(TEST_DB_URL, autocommit=True) as conn:
            cur = conn.cursor()

            cur.execute(f'DROP DATABASE IF EXISTS "{test_db_name}" WITH (FORCE)') # type: ignore
            cur.execute(f'CREATE DATABASE "{test_db_name}"') # type: ignore

            with psycopg.connect(TEST_DB_URL, dbname=test_db_name) as conn:
                yield conn

            cur.execute(f'DROP DATABASE IF EXISTS "{test_db_name}" WITH (FORCE)') # type: ignore

    yield _

Bonus : réécrire le luminaire de migration en tant que luminaire d'usine

Dans la partie précédente, j'ai également eu un appareil appliquant les migrations Yoyo à une base de données vide venant d'être créée. Ce n’était pas non plus très flexible. Faisons de même et enveloppons le code réel dans la fonction interne.

Dans ce cas, comme le code n'a pas besoin d'effectuer un nettoyage après le retour de la méthode de test (aucun rendement), le

  • Le luminaire d'usine renvoie (pas le rendement) la fonction interne
  • appels de luminaires spécialisés (pas de rendement) luminaire d'usine
@pytest.fixture
def make_yoyo():
    """Applies Yoyo migrations to test DB."""
    def _(test_db_name: str, migrations_dir: str):
        url = (
            urlparse(TEST_DB_URL)
            .
            _replace(scheme="postgresql+psycopg")
            .
            _replace(path=test_db_name)
            .geturl()
        )

        backend = get_backend(url)
        migrations = read_migrations(migrations_dir)

        if len(migrations) == 0:
            raise ValueError(f"No Yoyo migrations found in '{migrations_dir}'")

        with backend.lock():
            backend.apply_migrations(backend.to_apply(migrations))

    return _

@pytest.fixture
def yoyo_foo(make_yoyo):
    migrations_dir = str(Path(__file__, "../../foo/migrations").resolve())
    make_yoyo("test_foo", migrations_dir)

@pytest.fixture
def yoyo_bar(make_yoyo):
    migrations_dir = str(Path(__file__, "../../bar/migrations").resolve())
    make_yoyo("test_bar", migrations_dir)

Une méthode de test qui nécessite deux bases de données et leur applique des migrations :

from psycopg import Connection

def test_get_new_users_since_last_run(
        test_db_foo: Connection,
        test_db_bar: Connection,
        yoyo_foo,
        yoyo_bar):
    test_db_foo.execute("...")
    ...

Conclusion

Construire votre propre fabrique de luminaires en créant et en supprimant des bases de données pour la méthode Pytest est en fait un bon exercice pour pratiquer le générateur Python et le rendement/rendement des opérateurs.

J'espère que cet article vous a aidé avec votre propre suite de tests de base de données. N'hésitez pas à me laisser votre question dans les commentaires et bon codage !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn