Błędny wynik testu czy błąd wykonania testu - jaka to różnica?

W arsenale przykładowych projektów, które wykorzystuję podczas szkoleń, mam projekt pytest-intro, który zawiera pięć bardzo prostych testów w trzech scenariuszach testowych. Ostatnio podczas zajęć na uczelni zorientowałem się, że jeden z testów zawiera nieprawidłowy kod, który w przypadku błędnego działania testowanego serwisu może doprowadzić do błędnego wykonania testu (test error) zamiast do błędnego wyniku testu (test failure). Czym to się różni? Dobrze jest uczyć się na błędach, a najlepiej na cudzych, więc spieszę opisać wspomniany przypadek ze szczegółami.

Testy zaimplementowane w projekcie pytest-intro weryfikują spójność danych zapisanych w pliku CSV, działanie REST API serwisu GitLab oraz podstawową konfigurację aplikacji internetowej. Błąd znajdował się w jednym z testów dla API. Przewidywał on zweryfikowanie, czy w przesłanych w odpowiedzi danych w formacie JSON znajduje się niepusty element stats. Błędna implementacja była następująca:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest
import requests


@pytest.fixture()
def api_response():
"""Makes HTTP request to GitLab API"""
url = "https://gitlab.com/api/v4/projects/11463782/repository/commits"
payload = {"ref_name": "3f998241", "with_stats": True}
response = requests.get(url, params=payload)
return response


@pytest.fixture()
def json_content(api_response):
"""Gets HTTP response JSON content"""
data = api_response.json().pop() # pop() to extract data dict from one-element list
return data


def test_stats_is_not_empty(json_content):
"""Checks if JSON data contains stats item"""
assert json_content["stats"]

Gdzie jest błąd?

Błąd znajduje się w ostatniej linii powyższego kodu. Dane zwrócone przez fixture json_content są w postaci słownika, więc sprawdzenie assert json_content["stats"] będzie prawdą i test zakończy się poprawnym wynikiem tylko jeśli element stats będzie istniał i będzie zawierał niepuste dane (o wartości logicznej True). Jeśli element stats będzie zawierał puste dane (o wartości logicznej False, na przykład pusty ciąg znaków ""), to test zakończy się błędnym wynikiem, to znaczy pojawi się AssertionError. Ale zaraz… Jest jeszcze trzecia możliwość. Co jeśli elementu stats nie będzie w ogóle?

Wystarczy w danych payload zamienić wartość with_stats na False żeby przekonać się, że test zakończy się wówczas błędem wykonania w postaci KeyError. Stanie się tak dlatego, ponieważ w słowniku zawierającym json_content nie będzie wówczas w ogóle elementu o kluczu stats i kod próbujący odczytać wartość dla tego klucza zakończy się błędem:

Jak ten kod ulepszyć?

Należy użyć metody słownika get(), która również odczytuje wartość dla podanego klucza, ale w przypadku braku klucza w słowniku nie kończy się błędem, a zwraca alternatywną wartość, którą domyślnie jest None. Czyli kod assert json_content.get("stats") będzie fałszem zarówno wówczas, kiedy element stats będzie zawierał puste dane, jak i wtedy, kiedy nie będzie go wcale, bo metoda get() zwróci wówczas None. Wtedy test zakończy się błędnym wynikiem w postaci AssertionError:

W praktyce automatyzacji testów zawsze powinniśmy dążyć do tego, aby w dających się przewidzieć błędnych sytuacjach testy kończyły się błędnym wynikiem (test failure), to znaczy błędem asercji. Test zakończony błędem wykonania (test error) świadczy o wystąpieniu pewnych niespodziewanych okoliczności, które uniemożliwiły wykonanie właściwego testu, co z natury rzeczy wymaga dodatkowej analizy i niepotrzebnie marnuje nasz czas.

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.