Pytestarmaturer

Fuldført

Fikstures giver en defineret, pålidelig kontekst for tests. Specialindretninger er Python funktioner dekoreret med @pytest.fixture. Du bruger dem til at arrangere forudsætninger som testdata, miljøvariabler, databaseforbindelser eller filer, som en test har brug for, før den kører. Armaturer kan også definere nedtagningstrin, der rydder op i ressourcer, efter at testen eller armaturets scope er afsluttet.

En test anmoder om en fikstur ved at erklære et argument med samme navn som fixturen. Pytest finder fiksturen, kører den og sender den returnerede eller udgivne værdi ind i testen. For mere information om fiksturens livscyklus, se pytest-dokumentationen for fixtures.

Armaturer er mest hjælpsomme, når opsætning eller oprydning genbruges, er komplekse eller klarere, når de er navngivne. For simple enkeltstående værdier kan det være lettere at læse setup-koden direkte i testen.

Nøgleegenskaberne for pytestarmaturer omfatter:

  • Scope-kontrol: Fixtures kan bruge parameteren scope til at styre, hvor ofte pytest opretter og ødelægger fixture-værdien. Bredere omfang kan forbedre ydeevnen, men de deler også status i længere tid.
  • Opsætning og adskillelse: Armaturer kan bruges yield til oprydning. Avancerede armaturer kan også modtage den indbyggede request armatur (introduceret senere i denne enhed) for at inspicere den anmodende test og kalde request.addfinalizer() for at registrere oprydningsopkald.
  • Afhængighedsinjektion: Tests og fixtures anmodede om fixtures som argumenter, hvilket gør afhængigheder eksplicitte.
  • Genanvendelighed og modularitet: Fixtures kan defineres én gang og genbruges på tværs af testfunktioner, moduler, mapper eller projekter.
  • Automatisk aktivering: En fixture defineret med @pytest.fixture(autouse=True) kører for hver test inden for dens tilgængelighedsområde uden at blive opført som et argument. Brug denne mulighed sparsomt, fordi den skjuler testafhængigheder.

Oprettelse af et midlertidigt filarmatur

Når man skriver tests, der interagerer med filer, er det almindeligt at have brug for isolerede stier, der ikke kolliderer med andre tests eller er afhængige af en hardkodet placering. Pytest leverer indbyggede midlertidige mappe-fixtures, som normalt er det bedste valg til nye tests, fordi pytest opretter unikke mapper og håndterer deres opbevaring. Dog er det en nyttig måde at forstå fixture-anmodninger og returværdier på at oprette en lille tilpasset fixture.

Følgende fikstur returnerer en fabriksfunktion. Selve lampen opretter ikke en fil under opsætningen. I stedet opretter den indlejrede create() funktion en fil, når testen kalder tmp_file():

from pathlib import Path
import tempfile

import pytest


@pytest.fixture
def tmp_file():
    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        path.write_text(contents, encoding="utf-8")
        return path

    return create

Fixturen bruger Python's tempfile.NamedTemporaryFile()-funktion med delete=False, så filen stadig eksisterer, efter den er lukket. Filhåndtaget lukkes, før testen modtager stien, hvilket er vigtigt, når tests skal genåbne eller slette filen, især på Windows.

Her er et eksempel på, hvordan en test kunne bruge armaturet:

def test_file(tmp_file):
    path = tmp_file("ready")
    assert path.read_text(encoding="utf-8") == "ready"

I denne opsætning tmp_file er fixturens navn, og testen modtager den create() funktion, som fixturen returnerer. Kald tmp_file() inde i testen opretter filen, skriver det ønskede indhold og returnerer et pathlib.Path objekt for den.

Note

Vær forsigtig, når du kombinerer kampe med kontekstmanagere. Hvis en fixture returnerer en sti eller håndtag inde i en with blok, kan kontekstmanageren lukke eller slette ressourcen, før testen bruger den. Hold kontekstmanageren åben på tværs af a yield , når testen har brug for den administrerede ressource, eller luk ressourcen bevidst, før du vender tilbage, og ryd op senere.

Områdestyring

Som standard bruger function pytest-armaturer scope. En funktionsscoped fixture oprettes, når en test anmoder om den, og destrueres ved afslutningen af testen. Hvis armaturet bruger teardown-kode, kører pytest denne teardown-kode i slutningen af armaturets scope.

Pytest tillader også bredere fixture-scopes for at optimere dyrt opsætningsarbejde. De indbyggede navne på armaturets scope er:

  • function: Standardomfanget. Fiksturen oprettes én gang pr. testfunktion.
  • class: Inventaret oprettes én gang pr. testklasse.
  • module: Fiksturen laves én gang til modulet.
  • package: Fixturen oprettes én gang for pakken, hvor den defineres og skilles ned efter den sidste test i pakken, inklusive tests i underpakker og undermapper.
  • session: Fiksturen oprettes én gang til testsessionen.

For avancerede tilfælde kan der også være en callable, scope der returnerer et af disse scope-navne. Pytest kalder funktionen én gang, når den definerer fixturen, og sender nøgleordsargumenterne fixture_name og config. Se pytest-dokumentationen om dynamisk scope for detaljer.

I denne sammenhæng betyder 'skabt én gang ', at pytest cacher fixtureværdien for det pågældende scope. For eksempel kan en modul-scoped fixture anmodes af flere tests i ét modul, men disse tests modtager den samme cachede fixture-værdi. En parametriseret fixture er undtagelsen: pytest kan oprette en ny værdi for hvert parametersæt inden for scopet.

Hold armaturer funktions-scope, medmindre det er sikkert og nyttigt at dele opsætning. Ændrelige objekter, åbne forbindelser, miljøændringer og filer kan lække status mellem tests, når de caches i et bredere område.

Sådan ser tmp_file armatur ud med et modulomfang:

from pathlib import Path
import tempfile

import pytest


@pytest.fixture(scope="module")
def tmp_file():
    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        path.write_text(contents, encoding="utf-8")
        return path

    return create

Fordi denne fixture returnerer en fabriksfunktion, scope="module" cacher fabriksfunktionen for modulet. Fabrikken opretter stadig en ny fil hver gang en test kalder tmp_file(). Hvis fixturen oprettede og returnerede en filsti direkte, ville hver test i modulet modtage den samme sti.

Oprydningsstyring

De tidligere versioner af fixturen tmp_file opretter midlertidige filer, men de fjerner ikke disse filer. Pytest anbefaler yield armaturer til nem rengøring. Code udfører opsætningen før yield og leverer en værdi til testen. Kode efter yield kørsler under nedbrydning, selv når testen fejler efter opsætningen er færdig. Hvis en yield lampe hæves, før den når yield, kører pytest ikke nedbrydningskoden efter det yield, men den fjerner stadig tidligere armaturer, der blev gennemført med succes i samme test.

Følgende version sporer alle filer, der er oprettet af fabrikken, og forsøger at slette disse filer, når testen er afsluttet:

from pathlib import Path
import tempfile

import pytest


@pytest.fixture
def tmp_file():
    paths = []

    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        paths.append(path)
        path.write_text(contents, encoding="utf-8")
        return path

    yield create

    for path in paths:
        try:
            path.unlink()
        except FileNotFoundError:
            pass

Med standardscopet function kører pytest oprydningskoden efter hver test, der anmoder tmp_fileom . Hvis du ændrer armaturet til scope="module", kører pytest oprydningen under modulenedbrydningen, efter pytest er færdig med at køre tests i det modul. Når flere yield armaturer er aktive, kører pytest deres nedbrydningskode i omvendt rækkefølge af opsætningen.

Oprydningen bruges try/except FileNotFoundError , fordi en test måske selv sletter filen. På Windows kan sletning også fejle med PermissionError hvis en test har efterladt filen åben, så sørg for, at tests lukker alle håndtag, de opretter. Du kan også bruge request.addfinalizer() den til oprydning, når du har brug for mere kontrol. Registrer finalizeren først, efter ressourcen er oprettet, fordi pytest kører en tilføjet finalizer under nedtagning, selvom armaturet senere hæves. For en enkel oprydning yield er armaturerne som regel lettere at læse.

Brug af conftest.py

I stedet for at definere fixtures i hver testfil, kan du gemme delte fixtures i en fil kaldet conftest.py. Fixtures i conftest.py er automatisk tilgængelige for tests i samme mappe og dets undermapper uden eksplicitte import.

Du kan have flere conftest.py filer i en testsuite. Pytest løser fixture-tilgængelighed fra den anmodende tests perspektiv: fixturer i testklassen eller modulet betragtes i det lokale scope, derefter fixtures fra conftest.py filer i samme mappe og overordnede mapper. Tests kan søge opad efter armaturer, men de søger ikke nedad i søskende- eller børnekataloger. Plugin-armaturer søges efter lokale armaturer. Importer ikke fra conftest.py testfiler; lad pytest opdage det.

Udforskning af indbyggede installationer

Pytest har mange indbyggede armaturer, der er designet til at strømline test. Disse armaturer håndterer almindelige opsætnings- og oprydningsopgaver, så du kan fokusere på at skrive testpåstande. For at gennemgå den komplette liste, se pytest-referencen for indbyggede armaturer.

Vigtige indbyggede installationer omfatter:

  • cache: Gemmer og henter værdier på tværs af pytest-kørsler.
  • capsys: Fanger tekst skrevet til stdout og stderr.
  • tmp_path: Giver et pathlib.Path objekt til en midlertidig mappe, der er unikt for hver testfunktionskald, inklusive hver parametriseret sag.
  • tmpdir: Leverer en midlertidig mappe som et legacy-objekt py.path.local . Til nye tests anbefaler tmp_pathpytest .
  • monkeypatch: Ændrer midlertidigt klasser, funktioner, ordbøger, miljøvariabler og andre objekter.
  • request: Giver information om den aktuelt kørende test eller fixture og understøtter finalizere.

Andre almindeligt anvendte indbyggede installationer inkluderer caplog (opfanger logposter), capfd (fanger fil-deskriptor-niveau output), tmp_path_factory (opretter session-scoped midlertidige mapper) og pytestconfig (eksponerer den aktive pytest-konfiguration). For den fulde liste, se pytest-referencen for indbyggede armaturer.

Den næste øvelse bruger tmp_path og pathlib.Path til at oprette midlertidige filer i en per-test mappe. Du kan støde på tmpdir i eksisterende pytest-projekter, men for ny kode foretrækker tmp_pathdu . Som standard gemmer pytest midlertidige mapper fra de seneste testkørsler, så det forventes at se nylige pytest-administrerede midlertidige mapper efter et run. For mere information, se pytest-dokumentationen for midlertidige mapper og filer.

Rollen som abepatch i test

De tidligere armaturer skabte og ryddede op i testressourcerne. En anden almindelig anvendelse af armaturer er midlertidig tilstandsændring i tests, der skal isolere kode fra ekstern tilstand. I de tilfælde leverer pytest den indbyggede monkeypatch armatur, som i sig selv er en pytest-armatur og et godt eksempel på automatisk adskillelse.

At teste kode, der afhænger af eksterne ressourcer, global konfiguration eller operativsystemtilstand, kan være udfordrende. Fixturen monkeypatch lader dig midlertidigt ændre attributter, ordbogsværdier, miljøvariabler, den aktuelle arbejdsmappe eller sys.path til en test. Pytest fortryder automatisk disse ændringer, efter at den anmodede testfunktion eller fixture er færdig.

Her er et eksempel, der erstatter en funktionsattribut på et lille projektmodul for én test. Patching af snævert afgrænset applikationskode (i stedet for standardbiblioteksfunktioner, som pytest selv bruger) holder testen fokuseret og undgår utilsigtet at bryde pytests egen adfærd:

# project_module.py
def get_user_id():
    # Imagine this calls a remote service.
    return "real-user"
# test_project.py
import project_module


def test_user_id(monkeypatch):
    monkeypatch.setattr(project_module, "get_user_id", lambda: "test-user")
    assert project_module.get_user_id() == "test-user"

Du kan også specificere målet som en prikket importsti-streng. Pytest løser og importerer det navngivne modul, når patchen aktiveres, hvilket er praktisk, når testfilen ikke allerede har brug for modulet. Sidst import project_module i dette eksempel er kun for at testen kan referere til den patchede funktion for assertionen:

# test_project.py
def test_user_id_by_path(monkeypatch):
    monkeypatch.setattr("project_module.get_user_id", lambda: "test-user")

    import project_module
    assert project_module.get_user_id() == "test-user"

Udover at sætte attributter og overskrive metoder med setattr(), kan fixturen monkeypatch slette attributter med delattr(), sætte og slette miljøvariabler (setenv, ), delenvsætte og slette ordbogselementer (setitem, delitem), foransætte (sys.pathsyspath_prepend), ændre den aktuelle arbejdsmappe (chdir), og oprette et indlejret patching-scope med context(). Standardadfærden raising=True for setattr() og delattr() hjælper med at fange fejlstavede mål. Brug monkeypatch.context() den, når en lap skal fjernes, før resten af testen eller nedtagningen af armaturet fortsætter. For mere information, se pytest-dokumentationen for monkeypatching.

Selvom monkeypatch det er kraftfuldt, brug det forsigtigt:

  • Kodeklarhed og vedligeholdelse: Overforbrug monkeypatch eller brug af den på komplekse måder kan gøre tests sværere at forstå og vedligeholde.
  • Testvaliditet: Monkeypatching kan skabe tests, der består under kunstige betingelser, som adskiller sig meget fra produktionsadfærden.
  • Overafhængighed af implementeringsdetaljer: Test, der patcher interne implementeringsdetaljer, kan blive skrøbelige, når den underliggende kode ændres.
  • Fejlsøgningskompleksitet: Fejlfinding af tests, der bruger monkeypatch , kan være sværere, når en patch ændrer grundlæggende applikationsadfærd.

monkeypatch Brug den, når det hjælper med at isolere en fokuseret adfærd, og hold hver patch så lille og eksplicit som muligt.