Klasy testowe i metody

Ukończone

Oprócz pisania funkcji testowych narzędzie Pytest umożliwia korzystanie z klas. Jak już wspomniano, nie ma potrzeby dziedziczenia, a klasy testowe są zgodne z kilkoma prostymi regułami. Korzystanie z klas zapewnia większą elastyczność i możliwość ponownego użycia. Jak widać dalej, Pytest trzyma się z drogi i unika zmuszania do pisania testów w określony sposób.

Podobnie jak funkcje, nadal można zapisywać asercji przy użyciu instrukcji assert .

Tworzenie klasy testowej

Użyjmy rzeczywistego scenariusza, aby zobaczyć, jak klasy testowe mogą pomóc. Poniższa funkcja sprawdza, czy dany plik zawiera wartość "yes" w jego zawartości. Jeśli tak, zwraca wartość True. Jeśli plik nie istnieje lub jeśli zawiera wartość "nie" w jego zawartości, zwraca wartość False. Ten scenariusz jest typowy w zadaniach asynchronicznych, które używają systemu plików do wskazywania ukończenia.

Oto jak wygląda funkcja:

import os

def is_done(path):
    if not os.path.exists(path):
        return False
    with open(path) as _f:
        contents = _f.read()
    if "yes" in contents.lower():
        return True
    elif "no" in contents.lower():
        return False

Oto jak wygląda klasa z dwoma testami (po jednym dla każdego warunku) w pliku o nazwie test_files.py :

class TestIsDone:

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Uwaga

Metody testowe używają ścieżki /tmp dla tymczasowego pliku testowego, ponieważ jest to łatwiejsze do użycia w tym przykładzie. Jeśli jednak musisz używać plików tymczasowych, rozważ użycie biblioteki takiej jak tempfile ta, która może bezpiecznie je utworzyć (i usunąć). Nie każdy system ma katalog /tmp i ta lokalizacja może nie być tymczasowa w zależności od systemu operacyjnego.

Uruchomienie testów z flagą -v w celu zwiększenia szczegółowości pokazuje wyniki testów:

pytest -v test_files.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
cachedir: .pytest_cache
rootdir: /private/
collected 2 items

test_files.py::TestIsDone::test_yes PASSED                               [ 50%]
test_files.py::TestIsDone::test_no PASSED                                [100%]

============================== 2 passed in 0.00s ===============================

Mimo że testy przechodzą, wyglądają powtarzalnie i pozostawiają również pliki po zakończeniu testu. Zanim zobaczymy, jak możemy je ulepszyć, omówimy metody pomocnika w następnej sekcji.

Metody pomocnika

W klasie testowej istnieje kilka metod, których można użyć do konfigurowania i usuwania wykonania testu. Narzędzie Pytest wykonuje je automatycznie, jeśli są zdefiniowane. Aby użyć tych metod, należy wiedzieć, że mają one określoną kolejność i zachowanie.

  • setup: wykonuje raz przed każdym testem w klasie
  • teardown: wykonuje raz po każdym teście w klasie
  • setup_class: wykonuje raz przed wszystkimi testami w klasie
  • teardown_class: wykonuje raz po wszystkich testach w klasie

Gdy testy wymagają pracy podobnych (lub identycznych) zasobów, warto napisać metody konfiguracji. W idealnym przypadku test nie powinien pozostawiać zasobów po zakończeniu, więc metody usuwania danych mogą pomóc w czyszczeniu testów w tych sytuacjach.

Czyszczenie

Przyjrzyjmy się zaktualizowanej klasie testowej, która czyści pliki po każdym teście:

class TestIsDone:

    def teardown(self):
        if os.path.exists("/tmp/test_file"):
            os.remove("/tmp/test_file")

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Ponieważ użyliśmy teardown() metody, ta klasa testowa nie pozostawia już /tmp/test_file .

Ustawienia

Kolejną poprawą, którą możemy wprowadzić w tej klasie, jest dodanie zmiennej wskazującej plik. Ponieważ plik jest teraz zadeklarowany w sześciu miejscach, wszelkie zmiany w ścieżce oznaczałyby zmianę go we wszystkich tych miejscach. W tym przykładzie pokazano, jak klasa wygląda z dodaną setup() metodą, która deklaruje zmienną ścieżki:

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def test_yes(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("no")
        assert is_done(self.tmp_file) is False

Niestandardowe metody pomocnika

Niestandardowe metody pomocnicze można tworzyć w klasie. Te metody nie mogą być poprzedzone nazwą test i nie mogą być nazwane jako metody instalacji ani oczyszczania. TestIsDone W klasie możemy zautomatyzować tworzenie pliku tymczasowego w niestandardowym pomocniku. Ta niestandardowa metoda pomocnika może wyglądać następująco:

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

Narzędzie Pytest nie wykonuje write_tmp_file() automatycznie metody, a inne metody mogą wywołać ją bezpośrednio, aby zapisać je w powtarzających się zadaniach, takich jak zapisywanie w pliku.

Cała klasa wygląda jak w tym przykładzie po zaktualizowaniu metod testowych w celu użycia niestandardowego pomocnika:

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

    def test_yes(self):
        self.write_tmp_file("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        self.write_tmp_file("no")
        assert is_done(self.tmp_file) is False

Kiedy należy używać klasy zamiast funkcji

Nie ma żadnych rygorystycznych reguł dotyczących tego, kiedy należy używać klasy zamiast funkcji. Zawsze dobrym pomysłem jest przestrzeganie konwencji w bieżących projektach i zespołach, z którymi pracujesz. Poniżej przedstawiono kilka ogólnych pytań, które mogą pomóc w ustaleniu, kiedy należy użyć klasy:

  • Czy testy wymagają podobnej konfiguracji lub kodu pomocnika oczyszczania?
  • Czy grupowanie testów ma sens logiczny?
  • Czy w zestawie testów znajduje się co najmniej kilka testów?
  • Czy testy mogą korzystać ze wspólnego zestawu funkcji pomocnika?