Cześć! Kończymy pierwszy moduł praktycznym tematem, który będzie wracał w całym kursie. W poprzedniej lekcji rozróżnialiśmy błędy przewidywalne od nieoczekiwanych oraz używaliśmy try, except i raise, by nie ukrywać problemów. Teraz wykorzystasz te zasady do konfiguracji testów.
Test automatyczny nie powinien mieć na stałe wpisanego adresu środowiska, limitu czasu ani tokenu dostępowego. Te wartości różnią się między komputerem lokalnym, środowiskiem testowym i pipeline’em CI. Nauczysz się wczytywać zwykłą konfigurację z JSON-a, pobierać wartości zależne od środowiska z zmiennych środowiskowych oraz zgłaszać jasne błędy konfiguracji, zanim test wykona niepożądane żądanie.
Dwa rodzaje konfiguracji: ustawienia i sekrety
W projekcie testowym przydatne jest rozdzielenie dwóch kategorii danych.
| Kategoria | Przykłady | Zalecane miejsce |
|---|---|---|
| Ustawienia zwykle niesekretne | adres sandboxa, limit czasu, liczba ponowień | plik JSON w repozytorium |
| Dane zależne od środowiska lub wrażliwe | token API, hasło, klucz dostępu | zmienne środowiskowe |
| Dane testowe scenariusza | użytkownik, produkt, oczekiwany rezultat | osobny plik danych lub fabryka danych |
Przykładowo timeout_seconds może bezpiecznie znaleźć się w pliku config/test_config.json. Token TEST_API_TOKEN nie powinien się tam znaleźć, szczególnie jeśli plik jest wersjonowany przez Git. Sam format JSON nie zapewnia ochrony sekretu — jest zwykłym tekstem.
JSON jest wygodny, bo przechowuje uporządkowane dane: obiekty przypominające słowniki Pythona, listy, teksty, liczby i wartości logiczne. Python po odczycie zamienia obiekt JSON na słownik, a tablicę JSON na listę.
Najpierw utrwal składnię JSON-a. Ten fragment filmu „JSON – wszystko co musisz o nim wiedzieć” z kanału Twórca Stron pokazuje reguły, które najczęściej powodują błędy w plikach konfiguracji.
JSON - wszystko co musisz o nim wiedzieć
Obejrzyj fragment, aby rozpoznać poprawną strukturę obiektu JSON, typy wartości i typowe błędy składni.
W części struktura JSON skup się na różnicy między obiektem a tablicą, obowiązkowych podwójnych cudzysłowach dla kluczy i tekstów oraz na zakazie przecinka po ostatnim elemencie. Zwróć też uwagę na zapis true, false i null małymi literami.
Przykładowy plik config/test_config.json:
{
"base_url": "https://api.sandbox.example",
"timeout_seconds": 8
}
To jest konfiguracja domyślna projektu. Adres wskazuje fikcyjne środowisko sandboxowe, a nie produkcję. W realnym projekcie warto stosować jednoznaczne nazwy, takie jak sandbox, staging lub test, aby ograniczyć ryzyko wysłania danych testowych do niewłaściwego systemu.
Najczęstsze błędy JSON-a wyglądają tak:
{
'base_url': 'https://api.sandbox.example',
"timeout_seconds": 8,
}
Ten zapis jest niepoprawny, ponieważ używa pojedynczych cudzysłowów oraz ma przecinek po ostatnim polu. W przeciwieństwie do słownika Pythona JSON wymaga podwójnych cudzysłowów.
Odczyt pliku JSON w Pythonie
Do pracy z JSON-em Python ma wbudowany moduł json, więc nie trzeba instalować dodatkowej biblioteki. W praktyce najbezpieczniejszy jest wzorzec with, który zamyka plik także wtedy, gdy odczyt lub parsowanie zakończy się błędem.
Film „Python od podstaw (#4) Współpraca z plikami: TXT, CSV, JSON” z kanału Pasja informatyki pokazuje dwa elementy potrzebne w tym wzorcu: działanie with open(...) oraz odczyt JSON-a przez json.load().
Python od podstaw (#4) Współpraca z plikami: TXT, CSV, JSON
Obejrzyj dwa krótkie fragmenty, aby zobaczyć, dlaczego plik należy otwierać przez with i czym różni się odczyt pliku JSON od odczytu zwykłego tekstu.
W części blok with zobacz, jak menedżer kontekstu automatycznie zamyka plik po opuszczeniu wciętego bloku. Następnie przejdź do odczytu JSON: skup się na json.load() i na tym, że plik JSON po wczytaniu staje się strukturą słowników oraz list Pythona.
Warto zapamiętać rozróżnienie:
json.load(plik)odczytuje JSON bezpośrednio z otwartego pliku;json.loads(tekst)interpretuje JSON zapisany już w zmiennej typustr.
Dla konfiguracji zapisanej w pliku użyjesz prawie zawsze json.load().
Najkrótsza wersja odczytu wygląda następująco:
import json
from pathlib import Path
config_path = Path("config/test_config.json")
with config_path.open("r", encoding="utf-8") as file:
config = json.load(file)
print(config["base_url"])
print(config["timeout_seconds"])
Path z modułu pathlib reprezentuje ścieżkę do pliku. encoding="utf-8" warto podawać jawnie, zwłaszcza gdy konfiguracja może zawierać polskie znaki. Po wykonaniu json.load(file) zmienna config zawiera słownik.
Powyższy przykład działa przy założeniu, że:
- program uruchamiasz z katalogu głównego projektu;
- plik istnieje;
- JSON ma poprawną składnię;
- plik zawiera klucze o oczekiwanych nazwach.
W automatyzacji testów założenia trzeba sprawdzać. Błąd konfiguracji nie powinien zostać zamieniony na przypadkowy adres, pusty token ani ukryty komunikat w konsoli.
Czytelne błędy konfiguracji
Z poprzedniej lekcji znasz zasadę: przechwytuj tylko te błędy, na które umiesz sensownie zareagować. Przy pliku JSON są szczególnie istotne dwa przypadki:
FileNotFoundError, gdy wskazany plik nie istnieje;json.JSONDecodeError, gdy plik istnieje, ale jego zawartość nie jest poprawnym JSON-em.
Zbudujmy mały moduł config_loader.py. Nie musi jeszcze znać bibliotek HTTP ani pytest. Jego jedynym zadaniem jest wczytanie i sprawdzenie konfiguracji przed uruchomieniem testów.
import json
import os
from pathlib import Path
from typing import Any
class ConfigurationError(Exception):
"""Konfiguracja testów jest niekompletna lub niepoprawna."""
class TestSettings:
def __init__(
self,
base_url: str,
timeout_seconds: float,
api_token: str,
):
self.base_url = base_url
self.timeout_seconds = timeout_seconds
self.api_token = api_token
def load_json_object(path: Path):
try:
with path.open("r", encoding="utf-8") as file:
data = json.load(file)
except FileNotFoundError as error:
raise ConfigurationError(
f"Nie znaleziono pliku konfiguracji: {path}"
) from error
except json.JSONDecodeError as error:
raise ConfigurationError(
f"Niepoprawny JSON w pliku {path}, "
f"wiersz {error.lineno}, kolumna {error.colno}."
) from error
if not isinstance(data, dict):
raise ConfigurationError(
"Główną strukturą pliku konfiguracji musi być obiekt JSON."
)
return data
def require_text(value: Any, label: str):
if not isinstance(value, str) or not value.strip():
raise ConfigurationError(
f"{label} musi być niepustym tekstem."
)
return value.strip()
def require_positive_number(value: Any, label: str):
if isinstance(value, bool):
raise ConfigurationError(
f"{label} musi być dodatnią liczbą."
)
try:
number = float(value)
except (TypeError, ValueError) as error:
raise ConfigurationError(
f"{label} musi być dodatnią liczbą."
) from error
if number <= 0:
raise ConfigurationError(
f"{label} musi być dodatnią liczbą."
)
return number
def load_test_settings(config_path: Path):
config = load_json_object(config_path)
raw_base_url = os.getenv("TEST_BASE_URL")
if raw_base_url is None:
raw_base_url = config.get("base_url")
raw_timeout = os.getenv("TEST_TIMEOUT_SECONDS")
if raw_timeout is None:
raw_timeout = config.get("timeout_seconds")
base_url = require_text(
raw_base_url,
"TEST_BASE_URL lub klucz base_url",
)
timeout_seconds = require_positive_number(
raw_timeout,
"TEST_TIMEOUT_SECONDS lub klucz timeout_seconds",
)
api_token = require_text(
os.getenv("TEST_API_TOKEN"),
"TEST_API_TOKEN",
)
return TestSettings(
base_url=base_url.rstrip("/"),
timeout_seconds=timeout_seconds,
api_token=api_token,
)
Ten kod zawiera kilka ważnych decyzji projektowych.
1. Błąd ma wskazywać problem, nie tylko typ wyjątku
FileNotFoundError sam w sobie mówi, że czegoś brakuje. Nasz ConfigurationError dodaje kontekst: dokładną ścieżkę pliku konfiguracji. Podobnie JSONDecodeError zawiera numer wiersza i kolumny, więc łatwiej naprawić literówkę lub zbędny przecinek.
Zapis raise ... from error zachowuje pierwotną przyczynę w tracebacku. To bezpośrednie zastosowanie mechanizmu poznanego w poprzedniej lekcji.
2. Poprawny JSON nie gwarantuje poprawnej konfiguracji
Taki plik jest poprawnym JSON-em:
[
"https://api.sandbox.example",
8
]
Nie spełnia jednak naszego kontraktu, ponieważ loader oczekuje obiektu JSON, czyli słownika z nazwanymi polami. Dlatego po odczycie sprawdzamy isinstance(data, dict).
Podobnie ten JSON jest składniowo poprawny, ale konfiguracja nadal jest błędna:
{
"base_url": "",
"timeout_seconds": "bardzo szybko"
}
Adres jest pusty, a limitu czasu nie da się zamienić na liczbę. Funkcje require_text() i require_positive_number() zatrzymują program od razu z komunikatem dotyczącym konkretnego ustawienia.
3. Typy w JSON-ie i typy w środowisku różnią się
W JSON-ie liczba jest liczbą:
{
"timeout_seconds": 8
}
Po wczytaniu Python widzi 8 jako int. Natomiast każda zmienna środowiskowa jest tekstem. Jeśli ustawisz TEST_TIMEOUT_SECONDS=12.5, Python odczyta wartość "12.5" typu str. Dlatego loader świadomie wykonuje float(value) i kontroluje możliwy ValueError.
Zmienne środowiskowe i ich zasięg
Zmienne środowiskowe to pary nazwa–wartość dostępne dla uruchomionego procesu. W Pythonie udostępnia je moduł os.
Masz dwa podstawowe sposoby odczytu:
import os
token = os.getenv("TEST_API_TOKEN")
oraz:
token = os.environ["TEST_API_TOKEN"]
os.getenv() zwraca None, gdy zmiennej nie ma. Dzięki temu możesz zdecydować, czy użyć wartości z JSON-a, wartości domyślnej lub zgłosić własny błąd.
os.environ["TEST_API_TOKEN"] natomiast zgłasza KeyError, gdy zmienna nie istnieje. Jest użyteczne, gdy brak wartości ma bezwarunkowo zatrzymać program, ale własny ConfigurationError zwykle daje osobie uruchamiającej testy bardziej zrozumiały komunikat.

Diagram „Zasięg zmiennych środowiskowych” wyjaśnia częste źródło nieporozumień. Proces uruchamiany z terminala otrzymuje zestaw zmiennych od procesu nadrzędnego. Jeśli kod Pythona ustawi os.environ["MY_VARIABLE"] = "bar", wartość będzie dostępna w tym samym procesie oraz w procesach uruchomionych później przez niego. Nie pojawi się jednak automatycznie w innym już działającym terminalu, edytorze ani równoległym procesie testowym.
Na macOS i Linuxie możesz ustawić zmienne dla bieżącego terminala:
export TEST_API_TOKEN="przykladowy-token"
export TEST_BASE_URL="https://api.sandbox.example"
export TEST_TIMEOUT_SECONDS="12"
python run_check.py
W PowerShellu odpowiednik wygląda tak:
$env:TEST_API_TOKEN = "przykladowy-token"
$env:TEST_BASE_URL = "https://api.sandbox.example"
$env:TEST_TIMEOUT_SECONDS = "12"
python run_check.py
Te polecenia dotyczą bieżącej sesji terminala i procesów uruchomionych z niej później. Nie zapisują sekretu automatycznie jako trwałego ustawienia systemowego.
Łączenie źródeł i ustalanie pierwszeństwa
W load_test_settings() stosujemy prostą regułę pierwszeństwa:
| Ustawienie | Pierwsze źródło | Gdy go brakuje |
|---|---|---|
base_url | TEST_BASE_URL | base_url z JSON-a |
timeout_seconds | TEST_TIMEOUT_SECONDS | timeout_seconds z JSON-a |
| token API | TEST_API_TOKEN | błąd konfiguracji |
To umożliwia trzymanie wspólnych ustawień w repozytorium, a jednocześnie uruchamianie tych samych testów na różnych środowiskach bez zmiany kodu i bez edytowania JSON-a.
Przykład użycia loadera:
from pathlib import Path
from config_loader import load_test_settings
settings = load_test_settings(
Path("config/test_config.json")
)
print(f"Adres API: {settings.base_url}")
print(f"Limit czasu: {settings.timeout_seconds}")
Celowo nie wypisujemy tokenu. W logu lokalnym może to być ryzykowne, a w systemie CI często prowadzi do ujawnienia sekretu w artefaktach, historii logów lub komunikacie o błędzie.
W przyszłości obiekt settings przekażesz do klienta API zamiast wpisywać wartości bezpośrednio w testach. Dzięki temu pojedyncza zmiana konfiguracji wpływa na cały zestaw testów, a nie wymaga poprawiania wielu plików.
Lokalny plik .env: wygoda, ale nie źródło prawdy dla sekretów w CI
W lokalnej pracy ręczne ustawianie wielu zmiennych w terminalu bywa niewygodne. Częstą praktyką jest prywatny plik .env:
TEST_API_TOKEN=przykladowy-token
TEST_BASE_URL=https://api.sandbox.example
TEST_TIMEOUT_SECONDS=12
Python nie odczytuje pliku .env samodzielnie. Biblioteka python-dotenv może wczytać te wpisy do środowiska bieżącego procesu:
from dotenv import load_dotenv
load_dotenv()
Wywołaj load_dotenv() na początku programu uruchamiającego testy, zanim użyjesz os.getenv(). Domyślnie python-dotenv nie nadpisuje wartości już obecnych w środowisku, więc zmienna przekazana przez terminal lub CI pozostaje ważniejsza.

Schemat „Źródła konfiguracji .env” pokazuje właściwy model mentalny: .env jest wygodnym sposobem zasilenia zmiennych środowiskowych lokalnie, a kod aplikacji nadal korzysta z jednego interfejsu, czyli os.getenv().
Dobre zasady pracy z .env:
- dodaj
.envdo.gitignore; - nie umieszczaj w nim prawdziwych danych produkcyjnych;
- możesz utworzyć wersjonowany plik
.env.examplez samymi nazwami zmiennych i wartościami przykładowymi; - w CI używaj mechanizmu sekretów dostarczanego przez platformę, a nie kopiuj prywatnego
.envdo repozytorium; - nie zakładaj, że zmienna środowiskowa jest całkowicie niewidoczna — nadal nie należy jej wypisywać w logach.
Przykładowy .env.example może wyglądać tak:
TEST_API_TOKEN=uzupelnij_lokalnie
TEST_BASE_URL=https://api.sandbox.example
TEST_TIMEOUT_SECONDS=8
Ten plik dokumentuje wymagane ustawienia, ale nie ujawnia sekretów.
Kontrola przed dalszą automatyzacją
Po zbudowaniu loadera sprawdź go ręcznie w kilku kontrolowanych sytuacjach:
- Ustaw
TEST_API_TOKEN, pozostaw poprawny JSON i uruchom kod — powinieneś otrzymać adres oraz limit czasu. - Zmień
timeout_secondsna0— loader powinien zgłosićConfigurationError. - Usuń
TEST_API_TOKEN— program powinien przerwać działanie przed wykonaniem jakiejkolwiek komunikacji z API. - Wstaw zbędny przecinek do JSON-a — komunikat powinien wskazać plik oraz miejsce błędu.
- Ustaw
TEST_TIMEOUT_SECONDSw terminalu — jego wartość powinna zastąpić wartość z JSON-a.
Taki moduł konfiguracji jest niewielki, ale chroni przed kosztownym błędem: testem uruchomionym na złym środowisku albo bez wymaganych danych dostępowych.
Najważniejsze wnioski:
- JSON nadaje się do uporządkowanej, zwykle niesekretnej konfiguracji projektu;
- plik konfiguracji wczytujesz przez
withorazjson.load(); - poprawna składnia JSON-a nie wystarcza — trzeba też walidować wymagane klucze, typy i zakresy wartości;
- zmienne środowiskowe odczytujesz przez
os.getenv()lubos.environ; - wartości środowiskowe są zawsze tekstem, więc liczby wymagają jawnej konwersji i walidacji;
- środowisko może nadpisywać bezpieczne domyślne ustawienia z JSON-a, ale sekrety powinny być wymagane, nie domyślane;
- plik
.envjest narzędziem wygodnym lokalnie, lecz nie powinien trafiać do repozytorium.
W kolejnym module przejdziesz do pytest. Zaczniesz od struktury plików i funkcji, które pytest potrafi automatycznie wykryć oraz uruchomić.
Can't find a good explanation? Sign up and we'll make it for you
Sign up