Pytest - katalogi tymczasowe

Zdarza się, że test automatyczny potrzebuje tymczasowych struktur na dysku maszyny, na której jest wykonywany, na przykład katalogu na pliki zapisywane w trakcie pracy przez testowany program. Tego typu obiekty należy tworzyć w przemyślany sposób, żeby uniknąć niepotrzebnych zakłóceń wykonania testów, spowodowanych na przykład przez błędy pochodzące od systemu operacyjnego przy próbie utworzenia katalogu o nazwie, która już istnieje. Na szczęście język Python oraz framework Pytest posiadają mechanizmy, które bardzo takie zadanie ułatwiają.

Moduł tempfile

Python ma wbudowany moduł tempfile, który służy bezpiecznemu zarządzaniu tymczasowymi katalogami i plikami. Jego ogromną zaletą jest to, że jego funkcje działają zawsze tak samo, niezależne od platformy i sposobu działania danego systemu. Moduł ten domyślnie umieszcza tworzone struktury w lokalizacjach, które typowo służą do przechowywania tymczasowych danych (Windows: C:\Users\<username>\AppData\Local\Temp, Linux: /tmp) oraz dba o nadawanie im unikatowych nazw. Pozwala również takie struktury automatycznie usunąć po ich wykorzystaniu, a to dzięki zastosowaniu w jego klasach protokołu managera kontekstu.

Manager kontekstu

Manager kontekstu (ang. context manager) to konstrukcja w języku Python, która pozwala na łatwe i bezpieczne zarządzanie zasobami, w tym przede wszystkim na ich uwolnienie w momencie zakończenia lub przerwania kodu, który z nich korzysta. Manager kontekstu jest definiowany jako blok kodu rozpoczynający się słowem kluczowym with. Jednym z jego najbardziej popularnych zastosowań jest otwieranie plików z wykorzystaniem funkcji open:

1
2
with open(filepath) as f:
content = f.read()

Powyższy kod odczytuje zawartość pliku znajdującego się w ścieżce spod zmiennej filepath. Zastosowanie managera kontekstu zapewnia, że plik zostanie zawsze automatycznie zamknięty (a powiązane z nim zasoby uwolnione), niezależnie od tego, czy kod znajdujący się w bloku with wykona się prawidłowo, czy na przykład spowoduje wyjątek.

Tip: więcej na temat managera kontekstu znajdziesz w świetnym artykule Python with Context Managers na blogu Jeffa Knuppa oraz w dokumentacji języka Python na stronach opisujących Context Manager Types i moduł contextlib.

W przypadku modułu tempfile, konstrukcję managera kontekstu ze słowem kluczowym with możemy wykorzystać do automatycznego usunięcia tymczasowych danych kiedy przestaną być potrzebne. Poniższy przykład pokazuje tego typu operację wykonaną na tymczasowym katalogu:

1
2
3
4
5
import tempfile
with tempfile.TemporaryDirectory() as td:
print("Created a temporary directory", td)
print("Doing some stuff in the temporary directory...")
print("The temporary directory and its content are now gone")

Manager kontekstu wewnątrz fixture

We frameworku Pytest tymczasowy katalog najlepiej utworzyć w dedykowanej do tego celu fixture, która przekaże informacje o ścieżce katalogu do potrzebujących go funkcji testowych.

Warto pamiętać, że fixture standardowo może zawierać kod, który zostanie wykonany dopiero wtedy, gdy wszystkie korzystające z niej funkcje testowe zakończą działanie. To tak zwane kroki teardown, które umieszcza się po słowie kluczowym yield, zwracającym wartość z fixture. Możemy to wykorzystać do posprzątania tymczasowych struktur po testach.

Tip: więcej na temat kroków teardown znajdziesz w moim poprzednim artykule Pytest - zakres dla fixture i kroki teardown

Można powiedzieć, że wykonanie fixture zatrzymuje się na linii yield i jest kontynuowane po zakończeniu testów. W połączeniu z managerem kontekstu pozwala to na stworzenie bardzo ciekawej konstrukcji, w której po słowie kluczowym yield nie ma co prawda żadnego kodu, ale w rzeczywistości, zaraz po wykonaniu wszystkich zależnych testów, wykonywane są kroki zamykające managera kontekstu, które to na przykład… usuwają niepotrzebny już tymczasowy katalog :)

Jest to pokazane w poniższym bardzo prostym przykładzie, w którym funkcja testowa sprawdza, czy w utworzony przez fixture temp_dir tymczasowy katalog rzeczywiście istnieje w systemie plików.

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
import pytest
import tempfile


@pytest.fixture()
def temp_dir():
with tempfile.TemporaryDirectory() as td:
yield td


def test_temp_dir(temp_dir):
assert os.path.isdir(temp_dir)

Jeśli chcesz, możesz pobrać projekt zawierający powyższy kod i definicję koniecznych zależności klonując repozytorium:

git clone https://gitlab.com/qalabs/blog/pytest-tempdir-example.git

Jeśli nie masz Gita, możesz pobrać zip ze strony projektu https://gitlab.com/qalabs/blog/pytest-tempdir-example i rozpakować w dowolnym katalogu.

Współdzielenie struktur przez testy

Czasem może wystąpić sytuacja, w której bardziej optymalne lub wręcz konieczne będzie, aby testy współdzieliły tymczasowe struktury, na przykład wykorzystywały ten sam katalog. Aby to osiągnąć, można skorzystać z mechanizmu frameworku Pytest definiowania zakresu dla fixture.

Domyślny zakres to pojedyncza funkcja testowa. Oznacza to na przykład, że nowy tymczasowy katalog zostanie utworzony na początku każdego testu i usunięty zaraz po jego zakończeniu. Ale dla fixture można też podać szerszy zakres, dzięki czemu ten sam katalog będzie dostępny na przykład dla wszystkich testów w danej klasie, a nawet w całej sesji testowej (wszystkie możliwe wartości zakresu dla fixture to function, class, module, package, session).

Tip: więcej na temat definiowania zakresu dla fixtures znajdziesz w moim poprzednim artykule Pytest - zakres dla fixture i kroki teardown

Podsumowanie

Jak widać, dzięki funkcjonalnościom wbudowanym w język Python (moduł tempfile, manager kontekstu) oraz we framework Pytest (kroki teardown i zakres dla fixtures) zarządzanie tymczasowymi katalogami i plikami na potrzeby testów jest bardzo proste i efektywne - do tego stopnia, że przedstawiony powyżej minimalny działający przykład zawiera… zaledwie dziewięć linijek kodu.

Maciej Chmielarz

Nazywam się Maciej Chmielarz. Pracuję w branży informatycznej od 2008 roku. Doświadczenie zdobywałem u pracodawców należących do polskiej i światowej czołówki firm technologicznych, producentów unikatowych rozwiązań inżynierskich. Chętnie dzielę się wiedzą, prowadzę wykłady z testowania oprogramowania, występuję na konferencjach i spotkaniach branżowych oraz aktywnie uczestniczę w ich organizacji. Jestem pasjonatem cyberbezpieczeństwa i budowania świadomości informatycznej w społeczeństwie.