Pytest
to nowoczesny framework do uruchamiania testów automatycznych w języku Python. Może być stosowany do testów jednostkowych, ale sprawdza się bardzo dobrze również przy tworzeniu rozbudowanych testów wyższego poziomu (integracyjnych, end-to-end) dla całych aplikacji czy bibliotek.
Jego przemyślana konstrukcja ułatwia uzyskanie pożądanych efektów w sytuacjach o wysokim poziomie skomplikowania, a mnogość dostępnych rozszerzeń oraz łatwość tworzenia własnych bibliotek pozwala zastosować go do testowania produktów działających w praktycznie dowolnej technologii. Nie trzeba dodawać, że Pytest
radzi sobie bardzo dobrze również z testowaniem aplikacji webowych i usług sieciowych, a to dzięki wykorzystaniu bibliotek takich jak Selenium
i requests
.
Dokumentacja Pytest
Obszerna dokumentacja Pytest
, zawierająca liczne i bardzo dobrze opisane przykłady, jest dostępna na oficjalnej stronie projektu Pytest. Tworząc swój projekt automatyzacji oparty o ten framework, warto tę dokumentację przeczytać w całości (lub przynajmniej przejrzeć), żeby nabrać wyczucia odnośnie licznych funkcjonalności, oferujących gotowe rozwiązania dla najczęściej występujących problemów.
Przygotowanie środowiska
Do pracy z projektem opartym o Pytest
konieczne jest środowisko z zainstalowanym interpreterem języka Python, przy czym zdecydowanie zalecam Python 3 i na tej wersji będę bazował wszystkie przykłady. Aby sprawdzić, czy masz zainstalowany interpreter i czy jest on gotowy do użycia, z wiersza poleceń możesz wykonać komendę:
python --version
…co powinno wypisać na ekran numer zainstalowanej wersji interpretera (powinien zaczynać się od 3), na przykład:
Python 3.6.4
Oprócz tego przyda się zintegrowane środowisko programistyczne (Pycharm Community
lub Professional
) oraz opcjonalnie system kontroli wersji Git
.
Tip: Szczegółowe kroki instalacji
Git
w systemieWindows
zostały opisane w artykule Narzędzia - system kontroli wersji Git i emulator konsoli Cmder.
Przykładowy projekt
W dalszej części artykułu omówię po kolei kroki tworzenia prostego przykładowego projektu, ale już teraz możesz pobrać i sprawdzić jak działa jego końcowa wersja. Jeśli masz zainstalowany system kontroli wersji Git
, wystarczy że sklonujesz repozytorium:
git clone https://gitlab.com/qalabs/blog/pytest-parametrize-example.git
Tip: W repozytorium jest kilka commitów zawierających kolejne wersje plików opisanych w artykule. Jeśli chcesz przejść do konkretnego kroku i sprawdzić, jak działa skrypt na tym etapie, wystarczy jeśli zrobisz checkout odpowiedniego commita.
Jeśli nie masz Gita
, możesz pobrać zip
ze strony projektu https://gitlab.com/qalabs/blog/pytest-parametrize-example i rozpakować w dowolnym katalogu.
Tip: Chcesz szybko i skutecznie wystartować z Git? Proponujemy tekst, który przedstawia najbardziej istotne komendy na bazie prostego przykładowego projektu: https://blog.qalabs.pl/narzedzia/git-pierwsze-kroki/
Po sklonowaniu lub rozpakowaniu plików projektu musisz utworzyć i skonfigurować środowisko wirtualne virtualenv
. Aby to zrobić, najpierw z wiersza poleceń w katalogu projektu wykonaj komendę:
python -m venv venv
Następnie, aby aktywować środowisko wirtualne, wykonaj komendę:
(Windows) venv\Scripts\activate.bat
(Linux, MacOS X) source bin/activate
Zainstaluj zależności zdefiniowane w pliku requirements.txt
przy pomocy komendy:
pip install -r requirements.txt
Jeśli wszystko przebiegło pomyślnie, możesz uruchomić testy wpisując:
python -m pytest
Testowana funkcjonalność
Celem projektu, którym będę posługiwał się na potrzeby tego artykułu, jest automatyczne zweryfikowanie pliku eksportu w formacie CSV (ang. comma-separated values), wygenerowanego przez testowany system. Plik zawiera dane zapisane w formacie tekstowym, gdzie pierwszy wiersz stanowi nagłówek, pozostałe wiersze zawierają dane, a wartości są od siebie oddzielonyme przecinkami. Do zweryfikowania mamy następujący plik:
book.csv
1 | ID,AUTHOR,TITLE,PAGES,CREATED,UPDATED |
Wykonamy dla niego testy, które sprawdzą:
- Czy nazwy kolumn w nagłówku są zapisane wielkimi literami.
- Czy pierwsza kolumna w nagłówku to
ID
. - Czy nagłówek zawiera kolumnę
CREATED
- Czy nagłówek zawiera kolumnę
UPDATED
- Czy każdy rekord zawiera dane dla wszystkich kolumn z nagłówka.
- Czy pierwsza wartość każdego rekordu to liczba.
Pierwsze testy
Zaczniemy od utworzenia skryptu z sześcioma testami, które póki co nie będą niczego sprawdzać. Weryfikacja warunku testowego polega na użyciu słowa kluczowego assert
oraz wartości do sprawdzenia. Jeśli stanowi ona logiczną prawdę, to wynik testu jest poprawny, jeśli logiczny fałsz, to wynik jest błędny. Nasze testy póki co zawsze przechodzą, ponieważ sprawdzają assert True
. Posłużą nam jako formatka do uzupełnienia właściwym kodem.
1 | import pytest |
W celu uruchomienia testów należy w katalogu projektu w wierszu poleceń wykonać komendę python -m pytest
, co powinno dać wynik podobny do tego poniżej:
1 | ============================= test session starts ============================= |
Przygotowanie danych z użyciem fixtures
Każdy z testów wymaga podania danych wejściowych, które będzie mógł poddać analizie i weryfikacji. W powyższym przykładzie testy muszą mieć oczywiście dostęp do zawartości pliku CSV.
W Pytest
za przygotowanie danych dla funkcji testowych odpowiadają tak zwane fixtures, czyli specjalnie oznaczone funkcje, które są wykonywane automatycznie przed wykonaniem testów, które od nich zależą. Jeśli test potrzebuje danych z określonej fixture, można to wskazać przez umieszczenie nazwy tej fixture wśród parametrów w sygnaturze funkcji testowej. Poniższy przykład zawiera fixture o nazwie csv_data
, która wczytuje z dysku zawartość pliku book.csv
:
1 | import pytest |
Fixture otwiera plik book.csv
, odczytuje jego zawartość, dzieli ją względem znaków końca linii, zapisuje tak utworzoną listę do zmiennej data
i na koniec ją zwraca.
Teraz musimy dodać csv_data
do parametrów funkcji testowych, aby mogły skorzystać z tak przygotowanych danych. W poniższym kodzie znajduje się już również logika odpowiedzialna za poszczególne sprawdzenia:
1 | def test_header_is_uppercase(csv_data): |
Niektóre przedstawione wyżej operacje mogą wymagać wyjaśnienia. Przypisanie zmiennej header
zawartości wiersza nagłówkowego polega na odczytaniu elementu o indeksie 0 z danych przekazanych przez fixture csv_data
. Zapisanie nazw kolumn na liście column_names
odbywa się przez podzielenie wiersza nagłówkowego po przecinkach przy pomocy metody split
. Przypisanie zmiennej first_column_name
nazwy pierwszej kolumny polega na odczytaniu elementu o indeksie 0 z listy nazw kolumn.
Liczbę kolumn nagłówka w zmiennej header_columns_count
można określić jako długość listy z nazwami kolumn. W dwóch ostatnich testach inicjalizowana jest pusta lista errors
, która ma za zadanie przechować błędy zarejestrowane podczas analizy poszczególnych wierszy. Następnie pętle for
odczytują rekordy jeden po drugim, zaczynając od linii o indeksie 1 z csv_data
, a następnie dokonują właściwych sprawdzeń: porównania liczby wartości z liczbą kolumn nagłówka lub weryfikacji czy pierwsza wartość rekordu jest liczbą. Wynik testu jest pozytywny, jeśli lista błędów jest pusta.
Modułowa budowa fixtures
W powyższym skrypcie jest dużo zduplikowanego kodu: kilka razy jest odczytywany nagłówek, nazwy kolumn, rekordy itd. Tego typu powtórzeń należy unikać. To zła praktyka programistyczna, która może powodować problemy w dalszych pracach nad projektem. Na szczęście Pytest
wspiera modułową budowę fixtures, co pozwala na wygodne podzielenie kodu pomiędzy kilka funkcji i automatyczne wykonanie ich kiedy zajdzie taka potrzeba.
1 | import pytest |
Teraz każda funkcja testowa może użyć dokładnie tych danych, których potrzebuje, przygotowanych przez dedykowaną fixture.
1 | def test_header_is_uppercase(csv_header): |
W powyższym skrypcie zdefiniowane są cztery fixture oraz zależności pomiędzy nimi. W celu omówienia ich modułowej budowy i automatycznego wykonania posłużę się przykładem funkcji testowej test_header_starts_with_id
. Zależy ona od fixture column_names
i Pytest
wykona ją automatycznie przed wykonaniem testu. Ale nietrudno zauważyć, że fixture column_names
zależy od csv_header
, która to zależy od csv_data
, więc przed wykonaniem testu zostaną wykonane wszystkie te funkcje w odpowiedniej kolejności. Siła Pytest
polega na tym, że zrobi to automatycznie, na podstawie tych prosto zdefiniowanych zależności.
Statyczna parametryzacja fixtures
Automatyzacja testów jest najbardziej efektywna, kiedy raz napisany kod możemy użyć do wykonania wielu sprawdzeń. Ułatwia to wspierana przez Pytest
parametryzacja fixtures.
Załóżmy, że funkcjonalność eksportu do pliku CSV w testowanym przez nas systemie została wzbogacona o wsparcie dla nowych kategorii danych. Teraz musimy wykonać te same testy dodatkowo dla poniższych plików:
address.csv
1 | ID,STREET,CITY,COUNTRY,CREATED,UPDATED |
customer.csv
1 | ID,NAME,E-MAIL,PHONE,CREATED,UPDATED |
Na szczęście wykorzystując funkcjonalności Pytest
możemy to zrobić z niewielkimi zmianami w do tej pory napisanym skrypcie i bez niepotrzebnego duplikowania kodu. Wystarczy nieco zmienić fixture csv_data
jak poniżej:
1 |
|
Do dekoratora pytest.fixture
został dodany parametr params
z listą nazw plików, które mają zostać użyte w testach. Pytest
automatycznie wykona fixture tyle razy, ile wartości parametru zostało podanych. W ciele funkcji csv_data
dostęp do wartości parametru, czyli w tym przypadku nazwy pliku CSV, zapewnia obiekt request
poprzez atrubut param
. Został on podany jako argument bezpośrednio do funkcji open
, odpowiedzialnej za otwarcie pliku z danymi.
Najważniejsze jednak jest to, że Pytest
automatycznie identyfikuje wszystkie testy, które są bezpośrednio lub pośrednio zależne od takiej sparametryzowanej fixture i te testy również wykona dla każdej wartości parametru. Dlatego w raporcie z wykonania rzeczywiście widać po sześć testów wykonanych dla każdego z trzech plików (tym razem raport rozszerzony verbose
, uzyskany przy użyciu komendy python -m pytest -v
):
1 | ============================= test session starts ============================= |
Statyczna parametryzacja funkcji testowych
Pytest
umożliwia również parametryzację funkcji testowych, co możemy wykorzystać do dalszej optymalizacji kodu i pozbycia się niepotrzebnie zduplikowanych fragmentów. Można zauważyć, że dwie funkcje testowe - test_header_has_colum_created
oraz test_header_has_column_updated
- realizują właściwie taką samą funkcjonalność, różniącą się jedynie wartością sprawdzanej nazwy kolumny. Można zastąpić te dwie funkcje jedną i przy pomocy odpowiedniego dekoratora spowodować, żeby Pytest
sam cudownie je rozmnożył:
1 |
|
Nie wpłynęło to na liczbę i charakter wykonanych testów, co można zauważyć w szczegółowym wyniku:
1 | ============================= test session starts ============================= |
Dynamiczna parametryzacja funkcji
Wpisanie tego typu danych na sztywno w kod testowy nie zawsze jest dobrym pomysłem. Na przykład jeśli w przyszłości zostaną dodane kolejne pliki eksportu do CSV, będziemy musieli uzupełnić listę parametrów o kolejne nazwy. Można tego uniknąć i tutaj Pytest
również przychodzi z pomocą, dając możliwość dynamicznego parametryzowania fixtures i funkcji testowych przy pomocy specjalnej funkcji pytest_generate_tests
. Wykorzystamy ją do dynamicznego wygenerowania testów dla każdego pliku z rozszerzeniem CSV znajdującego się w katalogu projektu. Wystarczy zmienić początek skryptu jak poniżej:
1 | import os |
Na początku importujemy moduł os
, który umożliwia wykonywanie działań w systemie opreracyjnym. Funkcja pytest_generate_tests
wykorzystuje jego funkcję listdir
do pobrania nazw plików znajdujących się w katalogu projektu, a następnie zapisuje pliki, których nazwa kończy się na .csv
, na liście csv_files
. W kolejnym kroku dynamicznie przypsiuje wszystkim fixtures i funkcjom testowym zależnym od csv_file
listę parametrów w postaci listy nazw plików. Żeby parametryzacja mogła zadziałać, fixture csv_data
ma teraz zdefiniowaną zależność od parametru csv_file
. Wynik wykonania testów jest identyczny jak poprzednio, ale jeśli pojawią się kolejne pliki, nie trzeba będzie aktualizować listy z nazwami.
Podsumowanie
Przestawione powyżej przykłady pokazują oczywiście tylko ułamek możliwości frameworku Pytest
. Kwestie takie jak wykorzystanie konfiguracji conftest.py
, grupowanie testów w klasach, manipulowanie zakresem fixture, optymalizacja ścieżki wykonania, zaawansowane wykorzystanie asercji, pomijanie testów, dynamiczne modyfikowanie listy testów czy specyficzne podejście do teardown to tylko kilka spośród innych ważnych funkcjonalności.