Pytest - zakres dla fixture i kroki teardown

W poprzednim artykule na temat frameworku Pytest opisałem koncepcję fixtures oraz omówiłem podstawy statycznej i dynamicznej parametryzacji testów. Tym razem chciałbym się zająć wykorzystaniem funkcjonalności definiowania zakresu (scope) dla fixtures oraz wykonania kroków kończących (teardown) po zakończeniu testów. Jest to też okazja do zmierzenia się z prostymi testami REST API, wykonywanymi w języku Python z wykorzystaniem biblioteki requests.

Na podstawie materiału opisanego w tym artykule przeprowadziłem prezentację na spotkaniu TrójQA, nagranie można obejrzeć tutaj: Wierny, szybki, niezależny - dobry test automatyczny REST API (ale nie tylko).

SUT: Trello API

Jako system under test (SUT) do przedstawienia wspomnianych koncepcji wykorzystam REST API popularnej aplikacji Trello.

Dlaczego Trello?

Trello to serwis do zarządzania zadaniami o rozbudowanych funkcjonalnościach. Jest udostępniany jako aplikacja internetowa, aplikacja mobilna, oferuje też wspomniany dostęp przez REST API. Ze względu na proste do zrozumienia user journeys, nowoczesne interfejsy, mnogość metod dostępu i szeroki zakres darmowych opcji jest bardzo dobrym poligonem doświadczalnym do nauki automatyzacji testów. Dlatego skorzystam z niego do przedstawienia koncepcji poruszanych w tym artykule.

Dokumentacja API

Trello oferuje rozbudowane i dobrze udokumentowane REST API, z którym można zapoznać się w szczegółach na oficjalnej stronie dla programistów. Do testowania i prototypowania można użyć własnego konta w serwisie, dla którego należy utworzyć klucz i token dostępowy, zgodnie z instrukcją w dokumentacji API.

Tip: Klucz i token należy dobrze zabezpieczyć, ponieważ każdy, kto je zna, może podawać się za ich właściciela i poprzez API uzyskać pełny dostęp do jego konta w serwisie. Dlatego nie należy ich umieszczać w skryptach i dostępnych publicznie repozytoriach.

Tworzenie nowej karty

Jak wspomniałem, Trello ma liczne funcjonalności, wśród najważniejszych można wymienić tworzenie tablic dla projektów (boards), umieszczanie na tablicach list zadań do wykonania (lists) oraz umieszczanie na listach samych zadań w postaci kart (cards).

Na potrzeby tego artykułu wykorzystam funkcjonalność tworzenia nowej karty na liście przy pomocy funkcji REST API opisanej na tej stronie w dokumentacji. Wcześniej, w ramach przygotowania środowiska, użyłem interfejsu aplikacji do utworzenia tablicy o nazwie pytest-board oraz umieszczenia na niej listy o nazwie pytest-list.

Przypadki testowe

Udane żądanie API utworzenia nowej karty metodą POST zwraca kod odpowiedzi HTTP 200 OK oraz JSON zawierający szczegółowe dane utworzonej karty:

{
  "id": "5b15bdc827709694d5be84e1",
  "badges": {
    "votes": 0,
    "attachmentsByType": {
      "trello": {
        "board": 0,
        "card": 0
      }
    },
    "viewingMemberVoted": false,
    "subscribed": false,
    "fogbugz": "",
    "checkItems": 0,
    "checkItemsChecked": 0,
    "comments": 0,
    "attachments": 0,
    "description": true,
    "due": null,
    "dueComplete": false
  },
  "checkItemStates": [],
  "closed": false,
  "dueComplete": false,
  "dateLastActivity": "2018-06-04T22:31:36.588Z",
  "desc": "Test Description",
  "descData": {
    "emoji": {}
  },
  "due": null,
  "email": null,
  "idBoard": "5b13083cc204756935699dd0",
  "idChecklists": [],
  "idList": "5b13084d232f56be3c69ae16",
  "idMembers": [],
  "idMembersVoted": [],
  "idShort": 1,
  "idAttachmentCover": null,
  "labels": [],
  "idLabels": [],
  "manualCoverAttachment": false,
  "name": "Test Name",
  "pos": 16384,
  "shortLink": "o7ysPcBy",
  "shortUrl": "https://trello.com/c/o7ysPcBy",
  "subscribed": false,
  "stickers": [],
  "url": "https://trello.com/c/o7ysPcBy/1-test-name",
  "limits": {}
}

W ramach testów sprawdzimy:

  1. Czy kod opowiedzi HTTP to 200 OK.
  2. Czy JSON z danymi utworzonej karty zawiera niepustą wartość parametru id.
  3. Czy JSON z danymi utworzonej karty zawiera wartość parametru name zgodną z żądaniem.
  4. Czy JSON z danymi utworzonej karty zawiera wartość parametru desc zgodną z żądaniem.

Konfiguracja projektu

Jeśli chcesz pobrać opisywany kod, możesz skorzystać z gotowego projektu znajdującego się w repozytorium: https://gitlab.com/qalabs/blog/pytest-fixture-scope-teardown-example. Aby uruchomić skrypty na swojej maszynie, skonfiguruj środowisko i projekt w sposób opisany w artykule Narzędzia - Python i PyCharm

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.

Pierwsza wersja testów

Zaczniemy od utworzenia skryptu zawierającego cztery funkcje testowe (niepowiązane ze sobą) oraz trzy fixtures, które na potrzeby testów wykonają kroki konieczne do utworzenia karty na liście pytest-list na tablicy pytest-board. Karta będzie miała nazwę Test Name i opis Test Description, a testy wykorzystają ją do zweryfikowania wymienionych wyżej warunków.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import json
import pytest
import requests
from http import HTTPStatus

TRELLO_API_URL = "https://api.trello.com/1/"


@pytest.fixture()
def trello_creds():
"""Gets key and token required to authenticate against API"""
with open("trello_creds.json") as f:
creds = json.load(f)
return creds


@pytest.fixture()
def trello_list_id(trello_creds):
"""Gets id for pytest-list used to create test cards"""
# get pytest-board id from all boards of credentials owner
boards_url = TRELLO_API_URL + "/members/me/boards"
response = requests.get(boards_url, params=trello_creds)
board_id, = [trello_board["id"] for trello_board in response.json() if trello_board["name"] == "pytest-board"]
# get pytest-list id from all lists on pytest-board
lists_url = TRELLO_API_URL + "/boards/{}/lists".format(board_id)
response = requests.get(lists_url, params=trello_creds)
list_id, = [trello_list["id"] for trello_list in response.json() if trello_list["name"] == "pytest-list"]
return list_id


@pytest.fixture()
def create_card_response(trello_creds, trello_list_id):
"""Creates card with test name and description using API"""
cards_url = TRELLO_API_URL + "cards"
query_params = {
"name": "Test Name",
"desc": "Test Description",
"idList": trello_list_id,
}
query_params.update(trello_creds)
response = requests.post(cards_url, params=query_params)
return response


def test_response_http_status_ok(create_card_response):
"""Tests if response status code denotes success"""
assert create_card_response.status_code == HTTPStatus.OK


def test_response_card_id_set(create_card_response):
"""Tests if response JSON has non-empty card id"""
assert create_card_response.json()["id"]


def test_response_card_name_correct(create_card_response):
"""Tests if name parameter in response JSON corresponds to request"""
assert create_card_response.json()["name"] == "Test Name"


def test_response_card_desc_correct(create_card_response):
"""Tests if desc parameter in response JSON corresponds to request"""
assert create_card_response.json()["desc"] == "Test Description"

Przeanalizujmy przedstawiony kod fragment po fragmencie.

Zarządzanie wrażliwą konfiguracją

1
2
3
4
5
6
@pytest.fixture()
def trello_creds():
"""Gets key and token required to authenticate against API"""
with open("trello_creds.json") as f:
creds = json.load(f)
return creds

Fixture trello_creds odczytuje klucz i token do API z pliku konfiguracyjnego trello_creds.json. W repozytorium znajduje się tylko szablon pliku, bez rzeczywistych danych. To sposób na zarządzanie wrażliwą konfiguracją. Umieszczenie jej w osobnym pliku pozwoliło mi na zabezpieczenie jej przed przypadkowym upublicznieniem. Użyłem do tego polecenia Git:

git update-index --skip-worktree trello_creds.json

Dzięki temu Git przestał “widzieć” zmiany w pliku z konfiguracją i mogłem w nim podać rzeczywiste dane dostępowe, bez obaw że przez przypadek dodam je do repozytorium (dalsze szczegóły dotyczące tego podejścia znajdziesz w dokumentacji oraz w tej odpowiedzi na Stack Overflow)

Pobranie identyfikatora listy

Jak już wspomniałem wyżej, w ramach przygotowania środowiska utworzyłem na swoim koncie tablicę pytest-board, a na niej listę pytest-list. Aby utworzyć kartę poprzez API, muszę znać identyfikator utworzonej listy. Mógłbym go pobrać raz i po prostu umieścić w kodzie, ale to nie jest dobre podejście, choćby dlatego, że identyfikator będzie inny dla każdej osoby, która będzie chciała uruchomić testy na swoim koncie.

Dlatego lepiej użyć API i na podstawie nazwy tablicy i nazwy listy pobrać potrzebny identyfikator dynamicznie. Ta operacja nie ma bezpośredniego związku z opisywanymi funkcjonalnościami Pytest, dlatego póki co potraktuję tę fixture jak “czarną skrzynkę” i ograniczę się do stwierdzenia, że trello_list_id odpowiada za wskazanie identyfikatora testowej listy. Szczegóły działania, dla zainteresowanych, wyjaśnię w dodatku na końcu artykułu.

Wysłanie żądania utworzenia karty

1
2
3
4
5
6
7
8
9
10
11
12
@pytest.fixture()
def create_card_response(trello_creds, trello_list_id):
"""Creates card with test name and description using API"""
cards_url = TRELLO_API_URL + "cards"
query_params = {
"name": "Test Name",
"desc": "Test Description",
"idList": trello_list_id,
}
query_params.update(trello_creds)
response = requests.post(cards_url, params=query_params)
return response

Fixture create_card_response odpowiada za wykonanie żądania utworzenia nowej karty i odebranie odpowiedzi serwera. Wykorzystuje podany w dokumentacji API adres zapisany w zmiennej cards_url. Wśród parametrów zapytania definiuje testową nazwę i testowy opis karty oraz identyfikator testowej listy, pozyskany przez fixture trello_list_id. Po dodaniu do parametrów klucza i tokenu, dostarczonych przez fixture trello_creds, wykonuje żądanie metodą POST i zwraca odpowiedź serwera, w postaci instancji obiektu klasy requests.Response, zawierającą wszystkie dane potrzebne do jej zweryfikowania.

Właściwe testy

Funkcje testowe są bardzo proste i ograniczają się do wykonania odpowiednich asercji na danych dostarczonych przez fixture create_card_response. Po pierwsze, przyrównują kod uzyskanej odpowiedzi do wartości HTTPStatus.OK. Po drugie, odczytują zwrócony JSON i sprawdzają, czy wartość id karty jest niepusta (a dokładnie, czy ma wartość logiczną True). Po trzecie i czwarte, przyrównują parametry name i desc ze zwróconego JSON do wartości podanych w żądaniu utworzenia karty.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test_response_http_status_ok(create_card_response):
"""Tests if response status code denotes success"""
assert create_card_response.status_code == HTTPStatus.OK


def test_response_card_id_set(create_card_response):
"""Tests if response JSON has non-empty card id"""
assert create_card_response.json()["id"]


def test_response_card_name_correct(create_card_response):
"""Tests if name parameter in response JSON corresponds to request"""
assert create_card_response.json()["name"] == "Test Name"


def test_response_card_desc_correct(create_card_response):
"""Tests if desc parameter in response JSON corresponds to request"""
assert create_card_response.json()["desc"] == "Test Description"

Tip: W profesjonalnym programowaniu należy unikać używania tak zwanych magic numbers, czyli wartości (najczęściej) liczbowych, które nie wiadomo skąd się wzięły, trudno odgadnąć, co oznaczają, przez które kod staje się mniej czytelny i trudniejszy w utrzymaniu. W powyższej funkcji testowej mogłem z powodzeniem przyrównać kod odpowiedzi HTTP do liczby 200, ale dla zapewnienia lepszej czytelności, zamiast tego użyłem odpowiedniej stałej HTTPStatus.OK, dostarczonej przez wbudowany moduł http.

Wykonanie testów

Przygotowane testy można uruchomić z konsoli poleceniem:

pytest -v

Dzięki parametrowi -v (verbose) zobaczymy szczegółowe informacje na temat wykonanych testów, w tym nazwy poszczególnych funkcji testowych:

============================= test session starts =============================
platform win32 -- Python 3.6.5, pytest-3.6.0, py-1.5.3, pluggy-0.6.0
cachedir: .pytest_cache
rootdir: pytest-fixture-scope-teardown-example, inifile:
collected 4 items

test_create_new_card.py::test_response_http_status_ok PASSED             [ 25%]
test_create_new_card.py::test_response_card_id_set PASSED                [ 50%]
test_create_new_card.py::test_response_card_name_correct PASSED          [ 75%]
test_create_new_card.py::test_response_card_desc_correct PASSED          [100%]

========================== 4 passed in 8.09 seconds ===========================

Jak widać, wszystkie testy zakończyły się sukcesem. Ale rzut oka do interfejsu webowego ujawnia coś niepokojącego:

Wykonanie testów utworzyło na liście pytest-list cztery karty o tych samych tytułach. Spodziewaliśmy się raczej jednej karty, na której dokonane zostaną wszystkie sprawdzenia. Skąd więc wzięły się pozostałe trzy?

Jest to związane z zakresem (scope) ustawionym dla utworzonych fixtures. W Pytest domyślnym zakresem dla każdej fixture jest pojedyncza funkcja testowa, co oznacza, że przed wykonaniem każdej funkcji wykonywane są wszystkie fixture, na których polega. Czyli każda z naszych trzech fixture została wykonana cztery razy, dla każdej z czterech funkcji testowych. Dotyczy to także fixture create_card_response, która odpowiada za utworzenie testowej karty.

Aby się o tym przekonać, można na szybko dodać funkcję print, która wypisze na ekran informację o wykonaniu fixture create_card_response:

1
2
3
4
5
6
7
8
9
10
11
12
13
@pytest.fixture()
def create_card_response(trello_creds, trello_list_id):
"""Creates card with test name and description using API"""
cards_url = TRELLO_API_URL + "cards"
query_params = {
"name": "Test Name",
"desc": "Test Description",
"idList": trello_list_id,
}
query_params.update(trello_creds)
response = requests.post(cards_url, params=query_params)
print("\ncreate card request sent")
return response

Po (na razie) manualnym przywróceniu środowiska do stanu początkowego (usunięciu utworzonych kart) i wykonaniu testów z dodatkowym parametrem -s (dzięki któremu Pytest nie przechwyci komunikatów wypisywanych na ekran), w konsoli widać, że fixture w istocie została wykonana cztery razy:

============================= test session starts =============================
platform win32 -- Python 3.6.5, pytest-3.6.0, py-1.5.3, pluggy-0.6.0
cachedir: .pytest_cache
rootdir: pytest-fixture-scope-teardown-example, inifile:
collected 4 items

test_create_new_card.py::test_response_http_status_ok
create card request sent
PASSED
test_create_new_card.py::test_response_card_id_set
create card request sent
PASSED
test_create_new_card.py::test_response_card_name_correct
create card request sent
PASSED
test_create_new_card.py::test_response_card_desc_correct
create card request sent
PASSED

========================== 4 passed in 7.19 seconds ===========================

Na wynik testów w tym przypadku to nie ma wpływu, chociaż mimo wszystko nieco obniża ich wiarygodność, bo można sobie wyobrazić jakiś ekstremalny scenariusz, w którym na przykład tylko pierwsza utworzona karta nie ma prawidłowo wypełnionego identyfikatora id, a w ten sposób to nie zostałoby wychwycone.

Ale z nadmiarowym utworzeniem trzech kart jest związany dużo większy problem - niepotrzebnie wydłużony czas wykonania testów. Wykonanie każdego żądania do API jest kosztowne i związane z istotnym czasem odpowiedzi. Przez to test trwa niepotrzebnie prawie cztery razy dłużej (bo samo wykonanie asercji zajmuje już pomijalnie mało czasu).

Na szczęście Pytest daje nam narzędzia, które umożliwiają szybkie i łatwe naprawienie tego problemu.

Ustawienie zakresu dla fixture

Pytest posługuje się czterema zakresami, które można ustawić dla fixture: function (domyślny), class (wykonanie raz dla wszystkich funkcji w ramach klasy testowej), module (wykonanie raz dla wszystkich funkcji w ramach modułu, czyli w uproszczeniu katalogu z testami), session (wykonanie raz dla całej sesji testowej). Za ich pomocą można optymalizować wykorzystanie zasobów potrzebnych testom.

Zakres klasy testowej

Aby poradzić sobie z opisanym wyżej problemem, przeorganizuję funkcje testowe i umieszczę je wewnątrz klasy testowej nazwanej TestCreateNewCard:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TestCreateNewCard:

def test_response_http_status_ok(self, create_card_response):
"""Tests if response status code denotes success"""
assert create_card_response.status_code == HTTPStatus.OK

def test_response_card_id_set(self, create_card_response):
"""Tests if response JSON has non-empty card id"""
assert create_card_response.json()["id"]

def test_response_card_name_correct(self, create_card_response):
"""Tests if name parameter in response JSON corresponds to request"""
assert create_card_response.json()["name"] == "Test Name"

def test_response_card_desc_correct(self, create_card_response):
"""Tests if desc parameter in response JSON corresponds to request"""
assert create_card_response.json()["desc"] == "Test Description"

Umożliwi mi to dodanie do dekoratora dla fixture create_card_response niestandardowego zakresu class, dzięki czemu ta fixture zostanie wykonana raz dla wszystkich funkcji, które jej potrzebują i są zdefiniowane w ramach wspomnianej klasy testowej:

1
2
@pytest.fixture(scope="class")
def create_card_response(trello_creds, trello_list_id):

Hierarchia zakresów

W tej sytuacji fixtures trello_creds i trello_list_id nie mogą zachować domyślnego zakresu function, ponieważ obowiązuje tutaj hierarchia, to znaczy fixture o szerszym zakresie (np. class) nie może zależeć od fixture o węższym zakresie (np. function). Warto zauważyć, że wartości, które te fixtures dostarczają testom, nie zmieniają się przez cały czas wykonania testów, więc możemy im swobodnie zdefiniować zakres scope="session".

To zresztą dobry pomysł również dlatego, że odczytanie klucza i tokenu z pliku tekstowego też nie jest za darmo (w sensie wykorzystania zasobów) i nie ma potrzeby, aby wielokrotne wykonanie tej operacji spowalniało testy.

Wykonanie testów

Zaktualizowane testy można ponownie uruchomić z konsoli, z wynikiem jak poniżej:

============================= test session starts =============================
platform win32 -- Python 3.6.5, pytest-3.6.0, py-1.5.3, pluggy-0.6.0
cachedir: .pytest_cache
rootdir: pytest-fixture-scope-teardown-example, inifile:
collected 4 items

test_create_new_card.py::TestCreateNewCard::test_response_http_status_ok PASSED [ 25%]
test_create_new_card.py::TestCreateNewCard::test_response_card_id_set PASSED [ 50%]
test_create_new_card.py::TestCreateNewCard::test_response_card_name_correct PASSED [ 75%]
test_create_new_card.py::TestCreateNewCard::test_response_card_desc_correct PASSED [100%]

========================== 4 passed in 3.22 seconds ===========================

Podczas wykonania testów wyraźnie widać, że pierwszy z nich wykonuje się dłużej (to wtedy właśnie wykonywane jest żądanie do API), a kolejne przechodzą błyskawicznie (bo korzystają z pobranych wcześniej danych). Całkowity czas wykonania jest istotnie krótszy, a w interfejsie webowym powstała tylko jedna karta:

Jednakże nawet ta jedna karta, która pozostała po teście, wciąż oznacza, że środowisko nie zostało automatycznie przywrócone do stanu początkowego. Kolejne uruchomienia testów spowodują nagromadzenie takich pozostawionych kart. Każdy test automatyczny powinien po sobie posprzątać, aby zapewnić powtarzalne rezultaty oraz dowolność w kolejności wykonania.

Większość frameworków do testów automatycznych zapewnia możliwość zdefiniowania kodu teardown, czyli kroków wykonywanych po testach, w tym także “sprzątających” po nich. Pytest oczywiście również wspiera tę koncepcję. Wykorzystam tę funkcjonalność do rozwiązania problemu pozostawionej karty.

Dodanie kroków teardown

W Pytest wszystkie działania wykonywane wokół testów są obsługiwane przez fixtures. Dotyczy to nie tylko kroków przygotowawczych, takich jakie były przedstawione w przykładach powyżej, ale również tych kończących, które są wykonywane po zakończeniu wszystkich testów zależnych od danej fixture.

Najprostszą metodą zdefiniowania w fixture kodu teardown jest zastąpienie w niej słowa kluczowego return słowem yield. Wówczas wszystkie instrukcje, jakie znajdą się w ciele tej fixture poniżej yield, zostaną wykonane na zakończenie, po wszystkich testach.

Usunięcie karty po testach

Właściwym miejscem na dodanie kodu usuwającego kartę po wykonaniu testów jest ta sama fixture, która ją tworzy, czyli create_card_response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@pytest.fixture(scope="class")
def create_card_response(trello_creds, trello_list_id):
"""Creates card with test name and description using API"""
cards_url = TRELLO_API_URL + "cards"
query_params = {
"name": "Test Name",
"desc": "Test Description",
"idList": trello_list_id,
}
query_params.update(trello_creds)
response = requests.post(cards_url, params=query_params)
yield response
card_id = response.json()["id"]
requests.delete(cards_url + "/" + card_id, params=trello_creds)

Polecenie return response, zwracające obiekt requests.Response, zastąpiłem poleceniem yield response. Dodałem po nim dwie linie, które z odpowiedzi serwera odczytują identyfikator utworzonej karty oraz wykorzystują żądanie do API aplikacji w celu jej usunięcia przy pomocy metody DELETE.

Wykonanie testów

Wynik uruchomienia testów w konsoli jest identyczny, jak poprzednim razem. Najważniejszą zmianę można zaobserwować w interfejsie aplikacji po wykonaniu testów:

Jak widać, testowej karty nie ma, została usunięta, czyli w praktyce środowisko zostało przywrócone do stanu początkowego.

Podsumowanie

Wiele przydatnych funkcjonalności frameworku Pytest jest skupionych wokół koncepcji fixtures. We wspomnianym na wstępie poprzednim artykule poruszyłem kwestie ich modułowej budowy oraz statycznej i dynamicznej parametryzacji. Tutaj przedstawiłem możliwości definiowania zakresu (scope) oraz dodawania kroków kończących (teardown). To potężne narzędzia, ułatwiające tworzenie wydajnych i łatwych w utrzymaniu testów. Ale na tym atrakcje się nie kończą, więc w przyszłości zapewne wrócę do tematu. W międzyczasie zapraszam do zapoznania się z dedykowaną stroną w dokumentacji Pytest: pytest fixtures: explicit, modular, scalable.

Pobranie id testowej listy

W post scriptum chcę się jeszcze pochylić nad fragmentem kodu, o którym wspomniałem wcześniej tylko ogólnie, a który umożliwia dynamiczne pobranie identyfikatora testowej listy. To kwestia specyficzna dla API Trello, nie jest konieczna do zrozumienia scope i teardown, stąd szczegółowe wyjaśnienie umieszczam dopiero tutaj, dla szczególnie zainteresowanych.

1
2
3
4
5
6
7
8
9
10
11
12
@pytest.fixture(scope="session")
def trello_list_id(trello_creds):
"""Gets id for pytest-list used to create test cards"""
# get pytest-board id from all boards of credentials owner
boards_url = TRELLO_API_URL + "/members/me/boards"
response = requests.get(boards_url, params=trello_creds)
board_id, = [trello_board["id"] for trello_board in response.json() if trello_board["name"] == "pytest-board"]
# get pytest-list id from all lists on pytest-board
lists_url = TRELLO_API_URL + "/boards/{}/lists".format(board_id)
response = requests.get(lists_url, params=trello_creds)
list_id, = [trello_list["id"] for trello_list in response.json() if trello_list["name"] == "pytest-list"]
return list_id

Implementacja testów zakłada, że w ramach przygotowania środowiska, w aplikacji zostały utworzone tablica pytest-board oraz lista pytest-list. W celu dynamicznego pobrania identyfikatora tej listy, skrypt wykorzystuje API aplikacji do pobrania wszystkich tablic bieżącego użytkownika i odnalezienia identyfikatora tablicy pytest-board, a następnie pobrania wszystkich list na tej tablicy i odnalezienia identyfikatora listy pytest-list.

Do odnalezienia i pobrania identyfikatora tablicy i listy użyłem dwóch interesujących mechanizmów języka Python: list comprehension oraz iterable unpacking. Przeanalizujmy poniższą linię kodu:

1
board_id, = [trello_board["id"] for trello_board in response.json() if trello_board["name"] == "pytest-board"]

Po prawej stronie przypisania wykonałem list comprehension, to znaczy utworzyłem nową listę (w sensie strukturę danych) na podstawie przefiltrowanej zawartości istniejącej listy. Istniejąca lista to w tym przypadku pobrana przez API lista wszystkich tablic, gdzie każdy element to słownik zawierający dane tablicy. Instrukcja ta oznacza dokładnie: przejdź po wszystkich elementach listy pobranych tablic (for trello_board in response.json()), zostaw na nowej liście te elementy, których nazwa to pytest-board (if trello_board["name"] == "pytest-board), a zamiast całego słownika, zostaw tylko wartość dla klucza id (trello_board["id"]).

Na utworzonej w ten sposób liście identyfikatorów spodziewam się tylko jednego elementu, to znaczy identyfikatora poszukiwanej tablicy. Najlepszą metodą pozyskania go jest użycie iterable unpacking i “odpakowanie” go do pojedynczej zmiennej, co zrobiłem po lewej stronie przypisania. Takie podejście nie tylko “wyciąga” pojedynczą wartość z listy, ale automatycznie weryfikuje, czy na liście rzeczywiście jest tylko jeden element. Jeśli będzie ich więcej lub lista będzie pusta, skrypt zostanie przerwany z powodu ValueError, jak w poniższych przykładach:

>>> a, = [1, 2]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 1)
>>> a, = []
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 1, got 0)
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.