Cześć! W poprzedniej lekcji uporządkowaliśmy kod w moduły i pakiety: funkcje budujące payloady mogą mieszkać w payloads.py, a kontrolki odpowiedzi w response_checks.py. Teraz dodamy kolejną formę organizacji kodu: klasy.
Klasy są użyteczne wtedy, gdy kod ma nie tylko wykonywać działanie, ale też pamiętać związany z nim stan. W automatyzacji testów takim stanem może być adres środowiska, token autoryzacyjny, limit czasu albo lista danych utworzonych w scenariuszu. Dzisiejszym celem jest zdefiniowanie klasy, utworzenie jej obiektu oraz świadome użycie konstruktora, atrybutów i metod instancji.
Klasa, obiekt, atrybut i metoda
Klasa jest definicją własnego typu obiektu. Określa, jakie dane obiekt będzie przechowywać i jakie działania będzie umiał wykonać.
Obiekt (lub instancja) jest konkretnym egzemplarzem klasy. Jeśli ApiClient jest klasą opisującą klienta API, to staging_client może być obiektem skonfigurowanym dla środowiska testowego.
W klasie spotkasz przede wszystkim:
- atrybuty instancji — dane należące do konkretnego obiektu, np.
base_url; - metody instancji — funkcje umieszczone w klasie, działające na danych konkretnego obiektu, np.
users_url(); __init__— specjalną metodę inicjalizującą obiekt podczas jego tworzenia. Potocznie często nazywa się ją konstruktorem.

Podstawowa definicja wygląda tak:
class ApiClient:
def status(self) -> str:
return "Klient jest gotowy"
Kilka konwencji ułatwia czytanie kodu:
- nazwy klas zapisuj stylem
PascalCase, np.ApiClient,UserFactory,TestSession; - nazwy metod i atrybutów zapisuj małymi literami z podkreśleniami, np.
build_payload,base_url; - po nazwie klasy stawiasz dwukropek, a jej zawartość musi być wcięta — tak jak ciało funkcji.
Sama definicja klasy jeszcze niczego nie uruchamia. Trzeba utworzyć obiekt:
client = ApiClient()
print(client.status())
Wywołanie ApiClient() tworzy nową instancję i przypisuje ją do zmiennej client. Kropka w client.status() oznacza: „wywołaj metodę status należącą do tego konkretnego obiektu”.
Obejrzyj teraz fragment filmu, który porządkuje pojęcia klasy, obiektu, atrybutu, metody oraz pokazuje mechanizm __init__.
Wprowadzenie do klas i metod, programowanie obiektowe [Python]
Film „Wprowadzenie do klas i metod, programowanie obiektowe [Python]” Adama Djellouliego przedstawia klasy na prostych przykładach, a następnie przechodzi do konstruktora i metod korzystających z danych obiektu.
Zacznij od podstawowych pojęć: skup się na rozróżnieniu atrybutów, czyli danych, od metod, czyli działań obiektu. Następnie obejrzyj inicjalizację i metody, gdzie pokazano przekazywanie wartości do __init__ oraz używanie ich w zwykłej metodzie instancji.
self: dostęp do bieżącego obiektu
Przyjrzyj się ponownie metodzie:
class ApiClient:
def status(self) -> str:
return "Klient jest gotowy"
Parametr self oznacza obiekt, na którym wywołano metodę. Nie przekazujesz go samodzielnie:
client = ApiClient()
client.status()
Python automatycznie przekazuje client jako pierwszy argument metody. W przybliżeniu:
client.status()
jest wywołaniem metody ApiClient.status dla instancji client.
self pozwala metodzie odczytywać i zmieniać atrybuty danego obiektu. Choć technicznie pierwszy parametr metody można nazwać inaczej, zawsze zapisuj self. Jest to standard Pythona i czytelny sygnał dla osoby przeglądającej kod.
Porównaj dwa fragmenty:
class ApiClient:
def show_base_url(self) -> str:
return base_url
class ApiClient:
def show_base_url(self) -> str:
return self.base_url
Pierwszy wariant nie ma dostępu do base_url należącego do obiektu. Python szukałby zwykłej zmiennej o tej nazwie w zakresie metody. Drugi wariant jasno wskazuje: użyj atrybutu base_url aktualnej instancji.
Konstruktor: obiekt gotowy do użycia
Załóżmy, że testy mają korzystać z różnych środowisk. Klient API powinien znać:
- bazowy adres aplikacji;
- opcjonalny token;
- limit czasu oczekiwania na odpowiedź.
Można byłoby utworzyć pusty obiekt, a później ręcznie przypisywać mu te wartości:
client = ApiClient()
client.base_url = "https://staging.example.test"
client.token = "sekretny-token"
To jednak łatwo prowadzi do błędów. Ktoś może zapomnieć o ustawieniu tokenu lub adresu, a obiekt będzie istniał, ale nie będzie kompletny.
Lepszym rozwiązaniem jest __init__. Python wywołuje tę metodę automatycznie przy tworzeniu obiektu:
class ApiClient:
def __init__(
self,
base_url: str,
token: str | None = None,
timeout_seconds: float = 10.0,
) -> None:
self.base_url = base_url.rstrip("/")
self.token = token
self.timeout_seconds = timeout_seconds
Tworzenie obiektu wygląda teraz tak:
staging_client = ApiClient(
base_url="https://staging.example.test",
token="test-token",
)
local_client = ApiClient(
base_url="http://localhost:8000",
timeout_seconds=3.0,
)
W konstruktorze występują dwa rodzaje nazw:
def __init__(self, base_url: str) -> None:
self.base_url = base_url
base_urlpo prawej stronie jest parametrem otrzymanym podczas tworzenia obiektu;self.base_urlpo lewej stronie jest atrybutem zapisywanym wewnątrz konkretnej instancji.
Po utworzeniu klienta można odczytać jego stan:
print(staging_client.base_url)
print(staging_client.timeout_seconds)
print(local_client.base_url)
print(local_client.timeout_seconds)
Każdy obiekt ma własne wartości atrybutów. Zmiana adresu w local_client nie zmieni adresu w staging_client.
Warto zapamiętać dwie reguły dotyczące __init__:
- Parametry inne niż
selfstają się argumentami wymaganymi podczas tworzenia obiektu, chyba że mają wartość domyślną. - Metoda
__init__przygotowuje stan obiektu i nie powinna zwracać własnej wartości. Jej adnotacja-> Noneto czytelna informacja, że nie zwraca wyniku.
Przykładowo ten kod zakończy się błędem, ponieważ nie podano wymaganego base_url:
client = ApiClient()
Natomiast ten jest poprawny:
client = ApiClient("https://api.example.test")
Metody instancji: zachowanie oparte na stanie
Atrybuty przechowują dane, a metody wykorzystują je do wykonania działania. Rozbudujmy klasę klienta o metody, które nie wysyłają jeszcze prawdziwego żądania HTTP, lecz przygotowują elementy potrzebne później w testach API:
class ApiClient:
def __init__(
self,
base_url: str,
token: str | None = None,
timeout_seconds: float = 10.0,
) -> None:
self.base_url = base_url.rstrip("/")
self.token = token
self.timeout_seconds = timeout_seconds
def users_url(self, user_id: int) -> str:
return f"{self.base_url}/users/{user_id}"
def authorization_header(self) -> dict[str, str]:
if self.token is None:
return {}
return {
"Authorization": f"Bearer {self.token}",
}
Użycie:
staging_client = ApiClient(
base_url="https://staging.example.test/",
token="test-token",
)
url = staging_client.users_url(42)
headers = staging_client.authorization_header()
print(url)
print(headers)
Wynik:
https://staging.example.test/users/42
{'Authorization': 'Bearer test-token'}
Zwróć uwagę na kilka decyzji:
rstrip("/")usuwa końcowy ukośnik z adresu bazowego, dzięki czemu metodausers_url()nie tworzy adresu z podwójnym ukośnikiem;users_url()otrzymuje dodatkowy parametruser_id, aleselfnadal pozostaje pierwszym parametrem definicji;authorization_header()używa tokenu zapisanego wcześniej wself.token;- metoda zwraca wartość przez
return, aby test lub inna część programu mogły ją wykorzystać.
W automatyzacji testów zwykle preferuj metody, które zwracają dane, zamiast od razu je wypisywać przez print(). Dzięki temu możesz je sprawdzać asercjami:
assert staging_client.users_url(42) == (
"https://staging.example.test/users/42"
)
assert staging_client.authorization_header() == {
"Authorization": "Bearer test-token",
}
Klasa nie zastępuje wszystkich funkcji. Jeśli funkcja jest stateless, na przykład buduje payload wyłącznie z przekazanych argumentów, zwykła funkcja w module nadal jest bardzo dobrym rozwiązaniem. Klasa ma sens, gdy kilka działań korzysta z tego samego, trwałego stanu.
Atrybuty instancji a współdzielony stan
Atrybuty zapisane jako self.nazwa należą do pojedynczej instancji:
class TestSession:
def __init__(self, environment: str) -> None:
self.environment = environment
self.created_user_ids: list[int] = []
def remember_created_user(self, user_id: int) -> None:
self.created_user_ids.append(user_id)
Możemy utworzyć dwie niezależne sesje:
smoke_session = TestSession("staging")
regression_session = TestSession("local")
smoke_session.remember_created_user(101)
regression_session.remember_created_user(202)
print(smoke_session.created_user_ids)
print(regression_session.created_user_ids)
Każda sesja zawiera oddzielną listę:
[101]
[202]
To szczególnie ważne w testach. Dane utworzone przez jeden scenariusz nie powinny przypadkowo trafiać do innego.
Uważaj natomiast na listy i słowniki tworzone bezpośrednio w ciele klasy:
class TestSession:
created_user_ids: list[int] = []
Taka lista jest atrybutem klasowym, więc byłaby współdzielona przez wszystkie obiekty TestSession. W testach może to powodować trudne do zdiagnozowania przeciekanie stanu między scenariuszami.
Dla danych zmienialnych, takich jak list, dict i set, twórz nową wartość w __init__:
class TestSession:
def __init__(self) -> None:
self.created_user_ids: list[int] = []
Najczęstsze błędy przy pierwszych klasach
Brak self w definicji metody
class ApiClient:
def users_url(user_id: int) -> str:
return f"/users/{user_id}"
Przy wywołaniu client.users_url(42) Python i tak przekaże obiekt jako pierwszy argument. W efekcie argumenty zostaną przesunięte i pojawi się błąd. Poprawna definicja to:
def users_url(self, user_id: int) -> str:
Utworzenie lokalnej zmiennej zamiast atrybutu
class ApiClient:
def __init__(self, base_url: str) -> None:
base_url = base_url.rstrip("/")
Ten kod tylko przypisuje wartość do lokalnego parametru. Po zakończeniu __init__ obiekt nie ma atrybutu base_url.
Potrzebujesz:
self.base_url = base_url.rstrip("/")
Ręczne przygotowywanie obiektu po utworzeniu
client = ApiClient()
client.base_url = "https://staging.example.test"
Jeśli base_url jest konieczny do działania klienta, wymagaj go w __init__. Obiekt powinien być poprawnie skonfigurowany od chwili utworzenia.
Zwracanie wartości z __init__
def __init__(self, base_url: str) -> str:
self.base_url = base_url
return base_url
__init__ nie służy do zwracania wyniku. Konstruktor inicjalizuje obiekt; wartości zwracają zwykłe metody, takie jak users_url().
Krótki standard dla klasy wspierającej testy
Przy projektowaniu prostej klasy do automatyzacji zastosuj następującą kolejność:
- Ustal, jaki stan obiekt ma pamiętać, na przykład adres środowiska i token.
- Przekaż wymagane dane do
__init__. - Zapisz je jako atrybuty przez
self.nazwa. - Dodaj metody, które wykorzystują ten stan do konkretnego działania.
- Twórz nową instancję wtedy, gdy potrzebujesz niezależnego stanu.
- Nie umieszczaj współdzielonych, zmienialnych danych jako listy lub słownika na poziomie klasy.
Nie musisz jeszcze budować rozbudowanego klienta HTTP. Na obecnym etapie najważniejsze jest opanowanie mechanizmu: klasa definiuje wspólny wzorzec, a każda instancja przechowuje własne dane i wykonuje metody w swoim kontekście.
Najważniejsze wnioski:
- Klasa opisuje dane i zachowania, a instancja jest konkretnym obiektem utworzonym na jej podstawie.
__init__jest wywoływane automatycznie podczas tworzenia obiektu i ustawia jego stan początkowy.selfoznacza bieżącą instancję; używasz go do zapisu i odczytu atrybutów, np.self.base_url.- Metody instancji są funkcjami należącymi do klasy i zwykle jako pierwszy parametr przyjmują
self. - Atrybuty instancji, takie jak
self.tokenczyself.created_user_ids, są niezależne dla każdego obiektu. - Unikaj współdzielonych list i słowników definiowanych bezpośrednio w ciele klasy, ponieważ mogą mieszać stan różnych testów.
W następnej lekcji zajmiesz się wyjątkami Pythona: rozpoznawaniem przewidywalnych błędów i obsługiwaniem ich tak, aby testy oraz kod wspierający dawały czytelne informacje diagnostyczne.
Can't find a good explanation? Sign up and we'll make it for you
Sign up