Øvelse

Fuldført

I denne øvelse vil du bruge pytest med @pytest.mark.parametrize til at teste en funktion, inklusive tilfælde, hvor der forventes en undtagelse. Derefter refaktorerer du en klassebaseret test fra xUnit-stil setup_method() og teardown_method() tilgang til en fastgørelse. Ved at bruge parametrisering og fixturer kan du skrive tests, der dækker flere tilfælde med mindre gentagelse og en klarere opsætning.

For hjælp til opsætning, se Python-dokumentationen for virtuelle miljøer, pip-dokumentationen for pip install og pytest-dokumentationen for installation af pytest. For flere detaljer om pytest-funktionerne i denne øvelse, se pytest-dokumentationen for parametrisering af testfunktioner, assertions om forventede undtagelser, fixtures, xUnit-lignende opsætning samt midlertidige mapper og filer.

Før du starter

Brug Python 3.10 eller nyere, som beskrevet i modulforudsætningerne. Hvis du allerede har et virtuelt miljø med pytest installeret og aktiveret, så fortsæt til trin 1. Ellers skal du oprette og aktivere et virtuelt miljø i den mappe, hvor du vil oprette test_advanced.py.

På Windows PowerShell:

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version

På macOS, Linux eller Windows-undersystem til Linux:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version

Versionskommandoen skal printe en pytest-version, lignende denne output:

pytest x.y.z

Installationskommandoerne fastgør ikke pytest til en bestemt version. Pip bruger pakkemetadata til at vælge en stabil pytest-udgivelse, der er kompatibel med Python-versionen i det virtuelle miljø.

Brug python -m pytest den gennem hele denne øvelse, så pytest kører fra det aktive miljø. Hvis Windows PowerShell blokerer Activate.ps1, se dokumentationen Python venv for PowerShell execution policy-muligheden, eller kør det virtuelle miljøs Python direkte for pip- og pytest-kommandoerne. Eksempel:

.\.venv\Scripts\python -m pip install --upgrade pip
.\.venv\Scripts\python -m pip install pytest
.\.venv\Scripts\python -m pytest -v test_advanced.py

Trin 1 – Tilføj en fil med test til denne øvelse

  1. I den samme mappe, hvor du kører pytest, opretter du en ny testfil kaldet test_advanced.py. Læg ikke filen i .venv-mappen . Tilføj følgende kode:

    import pytest
    
    
    def str_to_bool(string):
        normalized = string.lower()
        if normalized in ["yes", "y", "1"]:
            return True
        if normalized in ["no", "n", "0"]:
            return False
        raise ValueError(f"Cannot convert {string!r} to a boolean")
    

    Funktionen str_to_bool() accepterer en streng som input. Den returnerer True for anerkendte sande værdier, returnerer False for anerkendte falske værdier og hæver a ValueError for enhver anden værdi.

  2. Tilføj testene for funktionen str_to_bool() i den samme fil. Brug @pytest.mark.parametrize den til at teste de sande værdier først:

    @pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
    def test_str_to_bool_true(string):
        assert str_to_bool(string) is True
    
  3. Derefter skal du tilføje en anden test med de falske værdier:

    @pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
    def test_str_to_bool_false(string):
        assert str_to_bool(string) is False
    

    Der findes nu to testfunktioner, der dækker repræsentative input for både og TrueFalse returværdierne.

  4. Tilføj endelig en parametriseret test for værdier, som funktionen ikke bør acceptere. Brug pytest.raises() som kontekstmanager, så testen kun består, når str_to_bool() den forventede undtagelse aktiveres:

    @pytest.mark.parametrize("string", ["maybe", "2"])
    def test_str_to_bool_invalid(string):
        with pytest.raises(ValueError, match="Cannot convert"):
            str_to_bool(string)
    

    Argumentet match tjekker undtagelsesbeskeden med et regulært udtryk. Her tjekker testen kun den stabile del af beskeden, så den ikke afhænger af den præcise ugyldige værdi.

Seddel

For enkelhedens skyld holder denne øvelse koden under test og testene i samme fil. I virkelige Python-projekter er applikationskode og tests normalt opdelt i forskellige filer og mapper, såsom en src/-pakke og en tests/-mappe.

Trin 2 – Kør testene, og udforsk rapporten

Når du har tilføjet testene, kør pytest og inspicer outputtet. Brug flaget for øget ordstil (-v) så du kan se hver inputværdi behandles som en separat test.

På Windows PowerShell:

python -m pytest -v test_advanced.py

På macOS, Linux eller Windows-undersystem til Linux:

python -m pytest -v test_advanced.py

Outputtet bør være lignende følgende rapport:

============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 10 items

test_advanced.py::test_str_to_bool_true[Y] PASSED                        [ 10%]
test_advanced.py::test_str_to_bool_true[y] PASSED                        [ 20%]
test_advanced.py::test_str_to_bool_true[1] PASSED                        [ 30%]
test_advanced.py::test_str_to_bool_true[YES] PASSED                      [ 40%]
test_advanced.py::test_str_to_bool_false[N] PASSED                       [ 50%]
test_advanced.py::test_str_to_bool_false[n] PASSED                       [ 60%]
test_advanced.py::test_str_to_bool_false[0] PASSED                       [ 70%]
test_advanced.py::test_str_to_bool_false[NO] PASSED                      [ 80%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED                 [ 90%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED                     [100%]

============================== 10 passed in 0.01s ==============================

Dit operativsystem, Python-version, pytest-version, eksekverbar sti, rodmappe, plugin- og cachelinjer, samlings-progress-formatering, linjewrapping og runtime kan variere. Selvom du kun skrev tre testfunktioner, samlede pytest 10 tests, fordi hver parametriseret inputværdi kører som sin egen testcase.

Trin 3 - Refaktorerer opsætning og demontering til en lampe

  1. Tilføj en klassebaseret test til test_advanced.py filen. Denne test bruger xUnit-stil setup_method() og teardown_method() metoder til at oprette og fjerne en fil omkring hver testmetode:

    from pathlib import Path
    
    
    class TestFile:
    
        def setup_method(self):
            self.path = Path("done")
            self.path.write_text("1", encoding="utf-8")
    
        def teardown_method(self):
            self.path.unlink(missing_ok=True)
    
        def test_done_file(self):
            contents = self.path.read_text(encoding="utf-8")
            assert contents == "1"
    

    Denne test kan bestå sig, men den skriver et fast filnavn i den aktuelle arbejdsmappe og kræver separat oprydningskode. Hvis en testkørsel afbrydes eller tests kører samtidig, såsom med pytest-xdist eller overlappende lokale kørsler, kan denne tilgang efterlade forældede filer, overskrive en eksisterende fil med navnet færdig eller få tests til at påvirke hinanden.

  2. Tilføj en modulniveau-fixture, der bruger den indbyggede tmp_path fixture til at oprette en midlertidig fil. I næste trin opdaterer du klassen til at bruge denne fixture i stedet for opsætnings- og nedbrydningsmetoderne. For læsbarhed placeres denne modulniveau-fixture efter de parametriserede str_to_bool tests og før klassen.TestFile Pytest opdager modulniveau-armaturer uanset hvor de optræder i modulet, så positionen er for menneskelige læsere, ikke pytest:

    @pytest.fixture
    def tmp_file(tmp_path):
        def write():
            path = tmp_path / "done"
            path.write_text("1", encoding="utf-8")
            return path
        return write
    

    Fixturen tmp_file() bruger pytests tmp_path fixture, som giver en unik pathlib.Path midlertidig mappe for hver testfunktionskald, inklusive hver parametriseret case. Pytest administrerer den midlertidige mappe, så testen ikke afhænger af en hardkodet filsti.

  3. Slet den tidligere TestFile klasse, inklusive dens setup_method() og teardown_method() metoder, og erstat den med denne endelige version, der bruger fixturen i stedet for opsætnings- og nedtrækningsmetoderne:

    class TestFile:
    
        def test_done_file(self, tmp_file):
            path = tmp_file()
            contents = path.read_text(encoding="utf-8")
            assert contents == "1"
    

    Du kan også fjerne importen from pathlib import Path , du tilføjede i det forrige trin, fordi testen ikke længere opretter et Path objekt direkte.

Kontrollér dit arbejde

Nu burde du have en Python-fil kaldet test_advanced.py som indeholder:

  • En str_to_bool() funktion, der accepterer en streng og returnerer en boolesk værdi for genkendte sande og falske værdier.
  • Tre parametriserede tests for str_to_bool() funktionen: en der tester True værdier, en der tester False værdier, og en der tjekker ugyldige værdier hæver ValueError.
  • En speciallavet pytest-fixture, der bruger den indbyggede tmp_path fixture til at oprette en midlertidig færdig fil med noget indhold.
  • En enkelt TestFile klasse med én metode, der bruger den brugerdefinerede tmp_file fixture til at oprette og læse filen.
  • Ingen from pathlib import Path import og ingen setup_method()/teardown_method() metoder i den endelige version.

Den endelige fil bør ligne følgende:

import pytest


def str_to_bool(string):
    normalized = string.lower()
    if normalized in ["yes", "y", "1"]:
        return True
    if normalized in ["no", "n", "0"]:
        return False
    raise ValueError(f"Cannot convert {string!r} to a boolean")


@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
def test_str_to_bool_true(string):
    assert str_to_bool(string) is True


@pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
def test_str_to_bool_false(string):
    assert str_to_bool(string) is False


@pytest.mark.parametrize("string", ["maybe", "2"])
def test_str_to_bool_invalid(string):
    with pytest.raises(ValueError, match="Cannot convert"):
        str_to_bool(string)


@pytest.fixture
def tmp_file(tmp_path):
    def write():
        path = tmp_path / "done"
        path.write_text("1", encoding="utf-8")
        return path
    return write


class TestFile:

    def test_done_file(self, tmp_file):
        path = tmp_file()
        contents = path.read_text(encoding="utf-8")
        assert contents == "1"

Kør testfilen igen.

På Windows PowerShell:

python -m pytest -v test_advanced.py

På macOS, Linux eller Windows-undersystem til Linux:

python -m pytest -v test_advanced.py

Den endelige rapport skal vise 11 indsamlede tests: 10 tilfælde fra de parametriserede tests og én klassebaseret test.

============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 11 items

test_advanced.py::test_str_to_bool_true[Y] PASSED                        [  9%]
test_advanced.py::test_str_to_bool_true[y] PASSED                        [ 18%]
test_advanced.py::test_str_to_bool_true[1] PASSED                        [ 27%]
test_advanced.py::test_str_to_bool_true[YES] PASSED                      [ 36%]
test_advanced.py::test_str_to_bool_false[N] PASSED                       [ 45%]
test_advanced.py::test_str_to_bool_false[n] PASSED                       [ 54%]
test_advanced.py::test_str_to_bool_false[0] PASSED                       [ 63%]
test_advanced.py::test_str_to_bool_false[NO] PASSED                      [ 72%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED                 [ 81%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED                     [ 90%]
test_advanced.py::TestFile::test_done_file PASSED                        [100%]

============================== 11 passed in 0.01s ==============================

Alle prøver burde bestå uden fejl.