Cześć! W poprzedniej lekcji opisaliśmy funkcje i dane testowe za pomocą adnotacji typów, m.in. dict[str, object] i TypedDict. To dobry moment, aby przestać traktować plik Pythona jako jedyne miejsce dla całego kodu.
W automatyzacji testów funkcje budujące payloady, sprawdzające odpowiedzi czy przygotowujące dane szybko zaczynają być potrzebne w wielu scenariuszach. Kopiowanie ich między plikami testów prowadzi do duplikacji i rozbieżności. Dziś uporządkujesz taki kod w moduły i pakiety, nauczysz się wybierać styl importu oraz unikniesz kilku typowych problemów z importowaniem.
Moduł: jeden plik o jednej odpowiedzialności
Moduł to po prostu plik Pythona z rozszerzeniem .py. Jeśli masz plik payloads.py, Python rozpoznaje go jako moduł o nazwie payloads.
Wyobraź sobie, że kilka testów API potrzebuje tych samych danych użytkownika:
def build_user_payload(
email: str,
role: str = "viewer",
) -> dict[str, str]:
return {
"email": email,
"role": role,
}
Na początku można wkleić tę funkcję do każdego pliku testowego. Problem pojawia się, gdy zmienia się kontrakt API, na przykład serwer wymaga pola active. Trzeba wtedy pamiętać o poprawieniu każdej kopii funkcji. Łatwo jedną pominąć.
Lepsze rozwiązanie to umieszczenie funkcji w osobnym module:
projekt_testowy/
├── payloads.py
└── run_smoke_check.py
Plik payloads.py:
from typing import TypedDict
class UserPayload(TypedDict):
email: str
role: str
active: bool
def build_user_payload(
email: str,
role: str = "viewer",
active: bool = True,
) -> UserPayload:
return {
"email": email,
"role": role,
"active": active,
}
Plik run_smoke_check.py:
import payloads
payload = payloads.build_user_payload(
email="tester@example.test",
role="editor",
)
print(payload)
Polecenie:
python run_smoke_check.py
powinno wypisać:
{'email': 'tester@example.test', 'role': 'editor', 'active': True}
Najważniejsza obserwacja: instrukcja
import payloads
nie przenosi funkcji build_user_payload bezpośrednio do bieżącego pliku. Udostępnia nazwę modułu payloads, dlatego funkcję wywołujesz jako:
payloads.build_user_payload(...)
Kropka pokazuje, skąd pochodzi dana funkcja. W większym projekcie jest to istotna wskazówka dla osoby czytającej kod.
Obejrzyj teraz krótkie fragmenty filmu „Moduły i Pakiety w Pythonie” z kanału CodeBucket. Najpierw zobaczysz praktyczne utworzenie modułu i podstawowe warianty importu, a później przejście do pakietów oraz ochronę kodu uruchamianego bezpośrednio.
Moduły i Pakiety w Pythonie - Pełny kurs dla początkujących (moduły math i random, PIP, __name__) 📦
Film „Moduły i Pakiety w Pythonie” z kanału CodeBucket pokazuje, jak pliki z kodem stają się modułami, jak działa import oraz jak katalogi porządkują rosnący projekt.
Obejrzyj ideę modułów, aby utrwalić podział kodu na pliki i użycie notacji z kropką. Następnie obejrzyj style importu; zwróć uwagę, które nazwy trafiają do aktualnego pliku. Pomiń środkową część o modułach matematycznych i przejdź do pakietów, gdzie omawiana jest hierarchia katalogów. Na końcu obejrzyj wartość name, skupiając się na różnicy między importem a bezpośrednim uruchomieniem pliku.
Importuj świadomie: trzy przydatne formy
Python oferuje kilka form importu. Wszystkie mogą działać poprawnie, ale dają kod o innej czytelności i innym ryzyku konfliktów nazw.
1. Import całego modułu
import payloads
payload = payloads.build_user_payload("tester@example.test")
To najbezpieczniejsza forma, gdy wykorzystujesz kilka rzeczy z jednego modułu. Nazwa payloads zostaje widoczna przy każdym użyciu, więc łatwo ustalić pochodzenie funkcji.
Dobrze sprawdza się na przykład przy module z kilkoma funkcjami walidującymi:
import response_checks
assert response_checks.has_expected_fields(body, {"id", "email"})
assert response_checks.has_status_code(response, 201)
2. Import konkretnej funkcji
from payloads import build_user_payload
payload = build_user_payload("tester@example.test")
Ta forma jest zwięzła i rozsądna, gdy potrzebujesz jednej lub dwóch wyraźnie nazwanych funkcji. W testach będzie często spotykana.
Jej wadą jest to, że źródło nazwy nie jest widoczne przy wywołaniu. Ponadto importowana funkcja może kolidować z funkcją lokalną albo funkcją zaimportowaną z innego modułu.
from payloads import build_user_payload
def build_user_payload(email: str) -> dict[str, str]:
return {"email": email}
W takim układzie lokalna definicja przesłania funkcję zaimportowaną. Kod może działać, ale niekoniecznie użyje tej implementacji, której oczekujesz.
3. Import z aliasem
Alias nadaje importowanej nazwie krótszą albo bardziej jednoznaczną formę:
from payloads import build_user_payload as build_user
Teraz używasz:
payload = build_user("tester@example.test")
Alias ma sens przede wszystkim wtedy, gdy:
- dwie importowane rzeczy mają tę samą nazwę;
- nazwa jest bardzo długa;
- chcesz usunąć niejednoznaczność.
Nie stosuj aliasów wyłącznie dla skracania każdej nazwy. Ten zapis:
import response_checks as rc
jest krótszy, ale po kilku tygodniach rc nie mówi wiele o roli modułu. W kodzie testowym czytelność jest zwykle ważniejsza niż kilka zaoszczędzonych znaków.
Czego unikać: importu z gwiazdką
Nie używaj w kodzie projektu:
from payloads import *
Taki import umieszcza w aktualnym pliku wiele nazw, których nie widać bez zaglądania do modułu. Zwiększa to ryzyko kolizji i utrudnia analizę kodu przez IDE oraz czytanie go przez zespół.
Zasada praktyczna:
- gdy korzystasz z kilku elementów, importuj moduł;
- gdy korzystasz z pojedynczego, jednoznacznego elementu, możesz użyć
from ... import ...; - unikaj
import *.
Oficjalny tutorial Pythona porządkuje te reguły i pokazuje zarówno moduły, jak i strukturę pakietów.
Przeczytaj odpowiednie fragmenty Rozdziału 6 oficjalnego tutorialu Pythona. Materiał wyjaśnia, jakie nazwy są dostępne po różnych wariantach importu, dlaczego import z gwiazdką jest niezalecany oraz jak Python rozumie pakiety.
W sekcji „6.1. Więcej o modułach” przeczytaj warianty importu, od importu wybranych funkcji do aliasów i informacji o jednokrotnym ładowaniu modułu. Następnie w sekcji „6.4. Pakiety” przeczytaj pakiety i importy: strukturę katalogów oraz różnicę między importem modułu a funkcji. Na końcu przejrzyj sekcję „6.4.2. Referencje wewnątrz-pakietowe”, zwłaszcza importy wzgledne; na razie wystarczy rozpoznać znaczenie pojedynczej i podwójnej kropki.
Pakiet: katalog dla powiązanych modułów
Gdy plików jest kilka, samo trzymanie ich obok testów przestaje wystarczać. Wtedy tworzysz pakiet, czyli logiczny katalog grupujący moduły o wspólnym celu.
Poniższa ilustracja przedstawia pakiet pkg, który zawiera dwa podpakiety. Każdy z nich grupuje własne moduły.

Dla prostego projektu automatyzacji API można zastosować taki układ:
projekt_testowy/
├── run_smoke_check.py
├── test_support/
│ ├── __init__.py
│ ├── payloads.py
│ └── response_checks.py
└── tests/
└── ...
Na tym etapie znaczenie elementów jest następujące:
| Element | Rola |
|---|---|
test_support/ | Pakiet z kodem wspólnym dla wielu testów. |
__init__.py | Oznacza zwykły, jawny pakiet Pythona; może być pusty. |
payloads.py | Budowanie danych wysyłanych do API. |
response_checks.py | Pomocnicze sprawdzanie odpowiedzi. |
tests/ | Miejsce, w którym później będą znajdować się testy pytest. |
W aktualnym Pythonie istnieją też tak zwane namespace packages, które mogą działać bez __init__.py. Nie są jednak potrzebne do zwykłego projektu testowego. Na początku umieszczaj pusty plik __init__.py w katalogu, który ma być Twoim pakietem — struktura i zamiar projektu będą jednoznaczne.
Przykład pakietu pomocniczego
Plik test_support/payloads.py:
from typing import TypedDict
class UserPayload(TypedDict):
email: str
role: str
active: bool
def build_user_payload(
email: str,
role: str = "viewer",
active: bool = True,
) -> UserPayload:
return {
"email": email,
"role": role,
"active": active,
}
Plik test_support/response_checks.py:
def has_expected_fields(
response_body: dict[str, object],
expected_fields: set[str],
) -> bool:
return expected_fields.issubset(response_body)
Plik run_smoke_check.py:
from test_support.payloads import build_user_payload
from test_support.response_checks import has_expected_fields
payload = build_user_payload(
email="tester@example.test",
role="editor",
)
response_body: dict[str, object] = {
"id": 42,
"email": "tester@example.test",
"role": "editor",
"active": True,
}
assert payload["role"] == "editor"
assert has_expected_fields(
response_body,
{"id", "email", "role", "active"},
)
print("Podstawowa kontrola zakończona powodzeniem.")
Uruchom skrypt z katalogu głównego projektu:
python run_smoke_check.py
Python widzi wtedy pakiet test_support, ponieważ katalog główny projektu jest punktem startowym uruchamianego programu. Jeśli zaczniesz uruchamiać przypadkowe pliki z wnętrza pakietu, importy mogą przestać działać, mimo że ich zapis wygląda poprawnie.
Dlatego nie rozwiązuj problemów z importami przez dopisywanie w kodzie:
sys.path.append(...)
Czasem spotkasz taki sposób w starych projektach, ale tworzy zależność od lokalnych ścieżek na komputerze autora. W dalszej części kursu spójne uruchamianie testów zapewnią struktura projektu, środowisko wirtualne i narzędzia testowe.
Co powinno znaleźć się w którym module?
Dobry podział nie polega na tworzeniu osobnego pliku dla każdej krótkiej funkcji. Chodzi o spójność odpowiedzialności.
W projekcie testów API rozsądny pierwszy podział może wyglądać tak:
test_support/
├── payloads.py # dane wejściowe do żądań
├── response_checks.py # funkcje sprawdzające odpowiedzi
├── date_helpers.py # daty, zakresy dat, formatowanie
└── config.py # odczyt ustawień testowych
Nie twórz natomiast modułu helpers.py dla wszystkiego. Taka nazwa szybko staje się „szufladą na różne rzeczy”, a po pewnym czasie nikt nie wie, gdzie znaleźć potrzebną funkcję.
Zamiast:
test_support/
└── helpers.py
preferuj nazwy mówiące o przeznaczeniu:
test_support/
├── payloads.py
├── response_checks.py
└── users.py
Przykładowo funkcja budująca dane użytkownika należy do payloads.py, a nie do modułu testu konkretnego endpointu. Dzięki temu może zostać użyta zarówno w teście tworzenia użytkownika, jak i w teście aktualizacji jego roli.
Nazwy modułów mają znaczenie
Stosuj małe litery i podkreślenia:
response_checks.py
user_factory.py
api_client.py
Unikaj nazw takich jak:
requests.py
json.py
typing.py
pytest.py
Mogą one przesłonić prawdziwe biblioteki o tych nazwach. Jeśli utworzysz własny plik requests.py, instrukcja:
import requests
może zaimportować Twój lokalny plik zamiast zewnętrznej biblioteki requests. Błędy tego rodzaju bywają mylące, ponieważ import formalnie się udaje, ale później brakuje oczekiwanych funkcji.
Kod uruchamiany przy imporcie
Import modułu nie oznacza wyłącznie udostępnienia funkcji. Python wykonuje instrukcje znajdujące się na najwyższym poziomie modułu przy jego pierwszym imporcie w danym procesie.
Rozważ plik test_support/payloads.py:
print("Ładowanie modułu payloads")
def build_user_payload(email: str) -> dict[str, str]:
return {"email": email}
Gdy inny plik napisze:
import test_support.payloads
komunikat zostanie wypisany automatycznie. To zwykle nie jest pożądane w kodzie pomocniczym testów.
Szczególnie unikaj na poziomie modułu:
- wysyłania żądań HTTP;
- tworzenia lub usuwania danych w aplikacji;
print()używanych do tymczasowej diagnostyki;- odczytu sekretów, jeśli można go wykonać dopiero wtedy, gdy dana funkcja jest faktycznie wywołana.
Jeżeli moduł ma być jednocześnie importowalny i możliwy do uruchomienia samodzielnie, użyj strażnika __name__:
def build_user_payload(email: str) -> dict[str, str]:
return {"email": email}
if __name__ == "__main__":
example = build_user_payload("demo@example.test")
print(example)
Przy uruchomieniu:
python test_support/payloads.py
warunek jest prawdziwy i zostanie wypisany przykład. Ale przy imporcie do innego modułu kod w bloku nie wykona się.
W praktyce ten wzorzec przydaje się do szybkiego sprawdzenia małego modułu. Później właściwe sprawdzanie funkcji przejmą testy pytest.
Importy wewnątrz pakietu
Moduły w tym samym pakiecie czasem muszą korzystać ze wspólnego kodu. Możesz wtedy użyć importu bezwzględnego:
from test_support.payloads import build_user_payload
albo importu względnego, zapisywanego wewnątrz pakietu:
from .payloads import build_user_payload
Pojedyncza kropka oznacza: „szukaj w bieżącym pakiecie”, czyli tutaj w test_support.
Dla początkującego projektu trzymaj się jednej, prostej konwencji:
- z plików testów importuj jawnie z pełną nazwą pakietu, np.
from test_support.payloads import build_user_payload; - wewnątrz pakietu możesz używać importów względnych, gdy wyraźnie pokazują lokalną relację modułów;
- nie uruchamiaj modułu zawierającego import względny bezpośrednio jako osobnego pliku.
Ostatni punkt jest ważny. Moduł z importem:
from .payloads import build_user_payload
zakłada, że działa jako część pakietu. Uruchomienie go wprost przez python response_checks.py pozbawia go tego kontekstu i może zakończyć się błędem importu.
Standard organizacji na obecnym etapie
Gdy piszesz kod wspierający testy, kieruj się następującym procesem:
- Zauważ funkcję lub strukturę danych powielaną w co najmniej dwóch miejscach.
- Nazwij jej odpowiedzialność, na przykład „budowanie payloadów użytkownika”.
- Umieść ją w module o nazwie wynikającej z tej odpowiedzialności.
- Jeśli powiązanych modułów jest więcej, zgrupuj je w pakiecie, takim jak
test_support. - Importuj tylko to, czego faktycznie używasz.
- Zostaw moduły pomocnicze bez efektów ubocznych wykonywanych przy imporcie.
Nie musisz już teraz tworzyć rozbudowanej architektury. Kilka małych, spójnych modułów jest lepszych niż jeden ogromny plik oraz lepszych niż kilkadziesiąt pustych katalogów „na przyszłość”.
Najważniejsze wnioski:
- Moduł to plik
.pyzawierający funkcje, klasy, stałe lub inne definicje. - Pakiet to katalog grupujący powiązane moduły; w zwykłym projekcie warto dodać do niego pusty plik
__init__.py. import modulzachowuje przestrzeń nazw i wymaga wywołania przezmodul.funkcja().from modul import funkcjajest wygodne dla kilku konkretnych, jednoznacznych elementów.- Unikaj
from modul import *, ponieważ ukrywa pochodzenie nazw i sprzyja kolizjom. - Nie nazywaj własnych modułów tak samo jak popularne biblioteki, np.
json.pyczyrequests.py. - Kod na najwyższym poziomie modułu uruchamia się przy imporcie; kod demonstracyjny osłaniaj przez
if __name__ == "__main__":.
W następnej lekcji przejdziesz do klas: zdefiniujesz konstruktor, atrybuty i metody instancji, które później posłużą między innymi do budowania czytelnych klientów API.
Can't find a good explanation? Sign up and we'll make it for you
Sign up