Jeszcze mniejszy framework do testów w Pythonie
Poniższy wpis jest kontynuacją poprzedniego wpisu, gdzie próbowałem pokazać Ci jak w Pythonie można stworzyć najmniejszy framework do testów.
Czym się dzisiaj wspólnie zajmiemy?
Otóż odpowiemy sobie na 2 pytania:
- Czy aby na pewno 16 linijek kodu, to najmniejsza możliwa ilość?
- Czy da się w tych 16 linijkach kodu stworzyć bardziej rozbudowany framework?
Odpowiedzi na te pytania zapewne się domyślasz, ale jeśli chcesz wiedzieć jak, to nie pozostaje Ci nic innego jak zagłębić się w dalszą część tego wpisu.
Gdzie skończyliśmy?¶
Dla małego przypomnienia zacznijmy w miejscu, gdzie skończyliśmy. Kod naszego mini frameworka wygląda tak:
import unittest, xmlrunner, json, requests
class Tests(unittest.TestCase): pass
def add_test(cls, name, data):
def abstract_test(self):
self.assertEqual(requests.request(**data['request']).status_code, data['assert']['statusCode'])
setattr(cls, name, abstract_test)
with open('tests.json', 'r') as json_file:
for test_name, test_data in json.load(json_file).items():
add_test(Tests, test_name, test_data)
if __name__ == '__main__':
unittest.main(testRunner=xmlrunner.XMLTestRunner())
W pogoni za rozumem¶
Kosmetyka¶
Póki co nasz kod jest w miarę czytelny, pomimo złamania kilku reguł. Skoro i tak je łamiemy, to idźmy ciut głębiej i uprośćmy ten kod do poniższej postaci.
import unittest, xmlrunner, json, requests
def add_test(cls, name, data):
def abstract_test(self):
self.assertEqual(requests.request(**data['request']).status_code, data['assert']['statusCode'])
setattr(cls, name, abstract_test)
with open('tests.json', 'r') as json_file:
class Tests(unittest.TestCase): pass
for test_name, test_data in json.load(json_file).items():
add_test(Tests, test_name, test_data)
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Zmian jest niewiele i to tylko kosmetyka:
- usunęliśmy sprawdzanie, z jakiego skryptu jest uruchamiany nasz kod,
- przerzuciliśmy definicję klasy
Test
do wnętrza blokuwith open(...)
.
Powyższa kosmetyka redukuję ilość linii kodu do 13.
Patrząc na kod, możemy zauważyć, że jednym z większych bloków jest definicja funkcji add_test
. Zastanówmy się czy da się coś z tym tematem zrobić.
Wyrażenia Lambda¶
Zanim jednak będziemy bardziej odchudzać kod frameworku, musimy poznać czym są tzw. wyrażenia Lambda w Pythonie.
Wyrażenia Lambda to krótkie, anonimowe funkcje (opisane w PEP 312 oraz dokumentacji). Funkcje takie są określane jako anonimowe, ponieważ zdefiniowane są w miejscu ich użycia. Funkcje Lambda mogą przyjmować dowolną liczbę argumentów, ale mogą zawierać tylko jedno wyrażenie. Ale jak działa Lambda?
Zacznijmy od czegoś prostego, czyli suma 2 liczb:
Nic nadzwyczajnego, ale żeby tradycji stało się zadość, kilka słów wyjaśnienia jak to działa:
- Tworzymy wyrażenie Lambda, które przyjmuje dwie zmienne:
a
ib
oraz wykonuje operację dodawania na tych zmiennych. - Wyrażenie to przypisujemy do zmiennej
add
. W tym momencie zmiennaadd
staje się funkcją, której ciałem jest wyrażenie Lambda (opisywałem ten mechanizm we wpisie dotyczącym dekoratorów, gdzie tłumaczyłem, że funkcja jest obiektem. - Wywołujemy tak stworzoną funkcję z 2 parametrami. W wyniku wywołania otrzymujemy wynik dodawania.
Odpowiednikiem takiego wyrażenia będzie poniższy kod:
Zysk niby niewielki, ale zawsze coś. A co, gdybyśmy chcieli osiągnąć coś ciekawszego? Zobaczmy co się stanie, jak tradycyjna metoda, w wyniku będzie zwracała wyrażenie lambda:
Zaczyna się robić ciekawiej. Zauważ, że w ten sposób moglibyśmy bardzo szybko zdefiniować inne metody np. multiplier_10k = multiplier(10000)
, czy też doubler = multiplier(2)
. Zaczynasz rozumieć z jak potężnym narzędziem mamy do czynienia?
Wszystko super, ale jak z tego skorzystać w naszym frameworku?
Lambda po raz pierwszy¶
Skoro już wiemy, że przy użyciu wyrażenia lambda, można zastąpić definicję jakiejś metody, to spróbujmy to zaimplementować w naszym kodzie. Jak? Zerknij poniżej:
import unittest, xmlrunner, json, requests
def add_test(cls, name, data):
setattr(cls, name, lambda self: self.assertEqual(requests.request(**data['request']).status_code, data['assert']['statusCode']))
with open('tests.json', 'r') as json_file:
class Tests(unittest.TestCase): pass
for test_name, test_data in json.load(json_file).items():
add_test(Tests, test_name, test_data)
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Myślę, że zmiany nie wymagają większego komentarza, bo jedyne co tutaj zrobiliśmy to zastąpiliśmy definicję oraz wywołanie metody abstract_test
wyrażeniem lambda. W wyniku, odchudziliśmy nasz kod do 11 linii kodu. Jest nieźle, ale może da się lepiej?
Lambda po raz drugi¶
Skoro udało nam się pozbyć definiowania jednej metody, to może uda nam się wyrzucić jeszcze jedną?
import unittest, xmlrunner, json, requests
add_test = lambda cls, name, data: setattr(cls, name, lambda self: self.assertEqual(requests.request(**data['request']).status_code,
data['assert']['statusCode']))
with open('tests.json', 'r') as json_file:
class Tests(unittest.TestCase): pass
for test_name, test_data in json.load(json_file).items():
add_test(Tests, test_name, test_data)
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Zauważ, że dalej podążamy utartym schematem. Wykonajmy jeszcze jeden drobny zabieg, a więc przenieśmy przypisanie Lambdy do zmiennej w inne miejsce:
import unittest, xmlrunner, json, requests
with open('tests.json', 'r') as json_file:
class Tests(unittest.TestCase): pass
add_test = lambda cls, name, data: setattr(cls, name, lambda self: self.assertEqual(requests.request(**data['request']).status_code, data['assert']['statusCode']))
for test_name, test_data in json.load(json_file).items():
add_test(Tests, test_name, test_data)
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Odchudziliśmy już nasz kod do 9 linii przy zachowaniu tej samej funkcjonalności. Czy to odchudzanie kiedyś się skończy? Tak, ale jeszcze nie w tym momencie. Zauważ, że sama definicja klasy Tests
, metody testowej add_test
oraz jej wielokrotne wywołanie zajmują aż 4 linijki kodu. Może damy radę jeszcze coś tutaj pokombinować?
type¶
Metoda type
to jedna z metod wbudowanych Pythona i nie wymaga importu (jest zawsze dostępna, podobnie jak używana przez nas metoda setattr
). Większość osób programujących w Pythonie używa tej metody do sprawdzania typu obiektu. Nie każdy jednak zdaje sobie sprawę, że type
można wykorzystać też w innym celu. type możemy również wykorzystać do dynamicznego tworzenia obiektów w Pythonie. Jak to działa? To nic skomplikowanego. W obecnym kodzie mamy następującą definicję klasy: class Tests(unittest.TestCase): pass
. Jest to klasa, dziedzicząca po unittest.TestCase
oraz nieposiadająca żadnych zmiennych, oraz metod. Przy użyciu type
powyższa definicja wyglądałaby tak: Tests = type("Tests", (unittest.TestCase,), {})
. Co tu się podziało?
- Przypisanie do zmiennej już znasz.
- Jako pierwszy argument podaliśmy nazwę nowego obiektu.
- Jako drugi argument podaliśmy tuple z obiektami, po jakich dziedziczy nowo tworzony obiekt.
- Jako trzeci argument podaliśmy pusty słownik, ponieważ nasza klasa jest 'pusta' (to nie do końca prawda, bo dziedziczy, po innym obiekcie, ale nie wnikajmy w to tutaj).
Po podstawieniu do naszego kodu, cały framework wyglądałby następująco:
import unittest, xmlrunner, json, requests
with open('tests.json', 'r') as json_file:
Tests = type("Tests", (unittest.TestCase,), {})
add_test = lambda cls, name, data: setattr(cls, name, lambda self: self.assertEqual(requests.request(**data['request']).status_code, data['assert']['statusCode']))
for test_name, test_data in json.load(json_file).items():
add_test(Tests, test_name, test_data)
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Na razie zysku brak. Ciągle mamy 9 linii kodu. Jednak dla wprawnego oka, przyzwyczajonego do kodu pisanego w Pythonie, widać, że coś tutaj możemy teraz uprościć.
dict comprehension¶
Zwrot dict comprehension nie posiada sensownego tłumaczenia (dlatego będę używał go w oryginale). Czym jest dict comprehension? Jest to sposób na uproszczenie zapisu, tworzenia słownika z wykorzystaniem pętli for
, który opisany jest w PEP 271 (istnieje również bardzo podobny mechanizm jak list comprehension, który opisany jest w PEP 202. W skrócie zapis:
keys = ["a", "b", "c", "d"]
values = [1, 2, 3, 4]
new_dict = {}
for i, k in enumerate(keys):
new_dict[k] = values[i]
new_dict
możemy zastąpić poprzez zapis:
keys = ["a", "b", "c", "d"]
values = [1, 2, 3, 4]
new_dict = {k: values[i] for i, k in enumerate(keys)}
new_dict
Oczywiście taka konstrukcja może być wykorzystywana też w połączeniu z warunkami itp. Ale jak to może pomóc w odchudzeniu naszego kodu? Trzymaj się krzesła.
Najmniejszy framework¶
Poskładajmy więc to wszystko w całość:
import unittest, xmlrunner, json, requests
with open('tests.json', 'r') as json_file:
tests_dict = {name: (lambda data: lambda self: self.assertEqual(
requests.request(**data['request']).status_code, data['assert']['statusCode']))(data)
for name, data in json.load(json_file).items()
}
Tests = type("Tests", (unittest.TestCase,), tests_dict)
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Jak to działa? Przy wykorzystaniu dict comprehension stworzyliśmy słownik tests_dict
, który zawiera wszystkie metody testowe, które wcześniej tworzone były wewnątrz pętli for
. Pominęliśmy również krok dotyczący definicji oraz wywołania metody add_test
. Nie jest ona w tym momencie konieczna, ponieważ przypisujemy metodę bezpośrednio do zmiennej, która jest wartością słownika, przypisaną do odpowiedniego klucza w tym słowniku. Tak zdefiniowany słownik wstawiamy do dynamicznie tworzonej klasy.
Powyższy kod został przedstawiony w ten sposób, aby choć trochę zachować jego czytelność. Jeśli jednak pozbędziemy się niepotrzebnego formatowania oraz przypisania słownika do zmiennej tests_dict
, nasz kod będzie wyglądał następująco:
import unittest, xmlrunner, json, requests
with open('tests.json', 'r') as json_file:
Tests = type("Tests", (unittest.TestCase,), {name: (lambda data: lambda self: self.assertEqual(requests.request(**data['request'
]).status_code, data['assert']['statusCode']))(data) for name, data in json.load(json_file).items()})
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Tak dobrze widzisz, że nasz framework mieści się w 6 linijkach kodu (pomijając puste linie, moglibyśmy odchudzić ten kod do zaledwie 4 linii kodu, co było by już tylko kosmetyczną zmianą). Nie wiem jak dla Ciebie, ale dla mnie to lekki obłęd.
Więcej funkcjonalności¶
Skoro odchudziliśmy kod frameworku do zaledwie 6 linijek kodu, to do pierwotnie zakładanych 16 linijek, trochę nam brakuje. Spróbujmy wykorzystać ten zapas, do stworzenia testów, które testują coś więcej.
Ale co tak naprawdę możemy dodać do naszego frameworka, aby był bardziej rozbudowany? Do głowy przychodzą mi 3 rzeczy:
- Dodanie weryfikacji poprawności struktury danych, jakimi odpowiada testowany endpoint poprzez weryfikację listy kluczy.
- Dodanie weryfikacji czasu odpowiedzi danego endpointa.
- Wsparcie dla wielu plików JSON, co da nam możliwość rozbicia testów do testowania mniejszych funkcjonalności lub grupowania testów jako suity testów.
Abyśmy mogli wprowadzić powyższe rozszerzenia, potrzebujemy delikatnie zmodyfikować obecny kod poprzez wydzielenie metody z testem z wnętrza słownika. Poniższy kod prezentuje jak tego dokonać:
import unittest, xmlrunner, json, requests
def abstract_test(self, data):
response = requests.request(**data['request'])
self.assertEqual(response.status_code, data['assert']['statusCode'])
with open('tests.json', 'r') as json_file:
test = lambda data: lambda self: abstract_test(self, data)
Tests = type("Tests", (unittest.TestCase,), {name: test(data) for name, data in json.load(json_file).items()})
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Zmiany, które zostały wprowadzone, nie powinny być zaskoczeniem, gdyż bardzo podobny kod był w akapicie dotyczącym wykorzystania lambdy.
Skoro mamy już podwaliny do dalszej zabawy, rozszerzmy kod o dodatkowe sprawdzenia.
Więcej testów w teście¶
Ponieważ wiemy już, co jeszcze chcemy testować, musimy znaleźć sposób na pobranie potrzebnych informacji. Okazuje się, że wszystko tak naprawdę już w naszym kodzie jest, ale jeszcze z tych informacji nie robimy użytku. Te i inne dodatkowe informacje otrzymujemy w odpowiedzi na wysłane żądanie. Wartości, z których możemy skorzystać w teście to:
resposne.json()
- zwraca słownik z danymi, którymi odpowiedział endpoint,response.elapsed.total_seconds()
- zawiera czas pomiędzy wysłaniem żądania, a otrzymaniem odpowiedzi.
Istnieją również inne ciekawe wartości, z których można skorzystać, np. response.headers
, ale nie zmieścilibyśmy się w wymaganej ilości kodu oraz musielibyśmy przechowywać dużo więcej informacji w pliku *JSON.
Abyśmy mogli z tych informacji skorzystać, musimy dołożyć pewne dane do naszego pliku tests.json
:
{
"test_get_all_users": {
"request": {
"method": "GET",
"url": "https://reqres.in/api/users"
},
"assert": {
"statusCode": 200,
"responseKeys": ["page", "total", "per_page", "total_pages", "data", "support"],
"responseTime": 0.300
}
},
"test_get_users_id_2": {
"request": {
"method": "GET",
"url": "https://reqres.in/api/users/2"
},
"assert": {
"statusCode": 200,
"responseKeys": ["data", "support"],
"responseTime": 0.300
}
},
"test_get_non_existing_user": {
"request": {
"method": "GET",
"url": "https://reqres.in/api/users/23"
},
"assert": {
"statusCode": 404,
"responseKeys": [],
"responseTime": 0.300
}
},
"test_create_new_user": {
"request": {
"method": "POST",
"url": "https://reqres.in/api/users",
"json": {
"name": "testerembyc",
"jon": "tester"
}
},
"assert": {
"statusCode": 201,
"responseKeys": ["name", "jon", "id", "createdAt"],
"responseTime": 0.300
}
}
}
Przeróbmy teraz nasz kod, tak aby skorzystał z tych danych:
import unittest, xmlrunner, json, requests, glob
def abstract_test(self, data):
response: requests.Response = requests.request(**data['request'])
self.assertEqual(response.status_code, data['assert']['statusCode'])
self.assertSetEqual(set(response.json().keys()), set(data['assert']['responseKeys']))
self.assertLessEqual(response.elapsed.total_seconds(), data['assert']['responseTime'])
with open('tests.json', 'r') as json_file:
test = lambda data: lambda self: abstract_test(self, data)
Tests = type("Tests", (unittest.TestCase,), {name: test(data) for name, data in json.load(json_file).items()})
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Sprawdzenie czasu odpowiedzi nie powinno być zaskoczeniem. Natomiast jeśli chodzi o porównanie struktury danych odpowiedzi wymaga drobnej ekwilibrystyki na danych:
- wyciągamy same klucze z odpowiedzi,
- następnie listę zamieniamy na
set
, - listę oczekiwanych kluczy również zamieniamy na
set
.
Zapytasz się po co to wszystko? Powód jest prosty, tzn. lista jest zbiorem uporządkowanych wartości i aby 2 listy były uznane za identyczne, obie listy poza zawieraniem tych samych wartości, muszą mieć jeszcze te wartości ułożone w takiej samej kolejności. set
natomiast w tym względzie jest mniej restrykcyjny i wymaga tylko posiadania takich samych wartości, nie przejmując się zupełnie ich kolejnością. Dzięki temu, zdecydowanie ułatwiamy sobie wprowadzanie danych testowych oraz eliminujemy fałszywe błędy spowodowane przez inną kolejność zwracanych przez endpoint wartości.
W tym momencie mamy 13 linii kodu, więc teoretycznie moglibyśmy coś tutaj jeszcze dorzucić, ale musimy pamiętaj jeszcze o kwestii związanej z obsługą dodatkowy plików json. Zanim to zrobimy, musimy wprowadzić dodatkową bibliotekę oraz omówić jedną dodatkową metodę, których użyjemy do tego zadania.
glob¶
Biblioteka glob w dużym uproszczeniu służy do wyszukiwania plików i katalogów. Na nasze potrzeby wykorzystamy tylko jedną metodę. a dokładniej glob.iglob. Wykorzystamy ja do wyszukiwania plików json.
globals()¶
globals() to kolejna z wbudowanych metod Pythona, która przechowuje zmienne globalne dla danego modułu. Na tym etapie, gdybyśmy w naszym kodzie wyświetli zawartość, którą zwraca globals()
, zauważylibyśmy m.in. coś takiego:
Jest to zmienna, w której przechowywana jest klasa z testami z pojedynczego pliku json. Po co nam to wiedzieć? Zapraszam dalej.
Obsługa wielu plików JSON¶
Wiemy już, czego będziemy używać, a więc do dzieła. Rozdzielmy najpierw obecny plik tests.json
na dwa mniejsze.
Pierwszy plik nazwiemy users_get.json
będzie testował API dotyczące pobierania danych użytkowników z testowanej aplikacji:
{
"test_get_all_users": {
"request": {
"method": "GET",
"url": "https://reqres.in/api/users"
},
"assert": {
"statusCode": 200,
"responseKeys": ["page", "total", "per_page", "total_pages", "data", "support"],
"responseTime": 0.300
}
},
"test_get_users_id_2": {
"request": {
"method": "GET",
"url": "https://reqres.in/api/users/2"
},
"assert": {
"statusCode": 200,
"responseKeys": ["data", "support"],
"responseTime": 0.300
}
},
"test_get_non_existing_user": {
"request": {
"method": "GET",
"url": "https://reqres.in/api/users/23"
},
"assert": {
"statusCode": 404,
"responseKeys": [],
"responseTime": 0.300
}
}
}
Drugi plik nazwiemy users_create.json
będzie testował API dotyczące tworzenia nowych użytkowników w testowanej aplikacji:
{
"test_create_new_user": {
"request": {
"method": "POST",
"url": "https://reqres.in/api/users",
"json": {
"name": "testerembyc",
"jon": "tester"
}
},
"assert": {
"statusCode": 201,
"responseKeys": ["name", "jon", "id", "createdAt"],
"responseTime": 0.300
}
}
}
Zauważ, że te pliki to tylko prosty podział i nie nastąpiła żadna modyfikacja danych, które oryginalnie zawarte były w pliku tests.json
.
Przejdźmy teraz do wprowadzenia zmian w kodzie:
import unittest, xmlrunner, json, requests, glob
def abstract_test(self, data):
response: requests.Response = requests.request(**data['request'])
self.assertEqual(response.status_code, data['assert']['statusCode'])
self.assertSetEqual(set(response.json().keys()), set(data['assert']['responseKeys']))
self.assertLessEqual(response.elapsed.total_seconds(), data['assert']['responseTime'])
for file_name in glob.iglob("*.json"):
with open(file_name, 'r') as json_file:
test = lambda data: lambda self: abstract_test(self, data)
suite_name = file_name.split('.')[0]
globals()[suite_name] = type(suite_name, (unittest.TestCase,), {name: test(data) for name, data in json.load(json_file).
items()})
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Wprowadziliśmy kilka drobnych zmian:
- Dodaliśmy pętle
for
, która iteruje po znalezionych plikach json. - Dodaliśmy zmienną
suite_name
, która przechowuje nazwę suity testów, a która jest nazwą pliku json bez jego rozszerzenia. - W dynamiczny sposób dodajemy zmienne globalne, które są oddzielnymi klasami testów, gdzie każdemu plikowi json odpowiada jedna klasa z testami.
Gdybyśmy teraz w naszym kodzie wyświetli zawartość, którą zwraca globals()
, zauważylibyśmy m.in. coś takiego:
Ile mamy linii kodu? 15. Uff. Dalej mieścimy się w zakładanych 16 linijkach kodu.
Podsumowanie¶
To była dosyć długa przygoda (aż dwa i to dosyć rozbudowane wpisy na blogu). Sporo Pythonowych trików, które na pierwszy rzut oka nie wydają się oczywiste, ale pokazują potęgę tego języka. Mam nadzieję, że ta podróż zachęci Cię do poznawania zarówno bibliotek Pythonowych jakie można wykorzystać w testach oraz wewnętrznych mechanizmów, jakie są w ten język wbudowane.
Bonus¶
Jeśli jesteś purystą i najważniejszą sprawą dla Ciebie jest zgodność kodu z PEP-8 to poniżej wersja tego frameworka, która jest z nim zgodna oraz w dalszym ciągu mieści się w 16 linijkach kodu (sprawdza status odpowiedzi oraz wspiera wiele plików JSON):
import unittest
import xmlrunner
import json
import requests
import glob
for file_name in glob.iglob("*.json"):
with open(file_name, 'r') as json_file:
tests_dict = {name: (lambda data: lambda self: self.assertEqual(
requests.request(**data['request']).status_code, data['assert']['statusCode']))(data)
for name, data in json.load(json_file).items()}
suite_name = file_name.split('.')[0]
globals()[suite_name] = type(suite_name, (unittest.TestCase,), tests_dict)
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Dodatkowo linki do repozytorium z najciekawszymi (według mnie) wersjami kodu, który omawialiśmy w tej mini serii znajdziesz poniżej:
Bonus 2¶
Po raz kolejny Jakub Spórna z bloga https://sporna.dev/ podrzucił jeszcze mniejszą wersję frameworka. Tym razem wziął na tapetę wersję najbardziej rozbudowaną funkcjonalność i postanowił ją troszeczkę bardziej pomniejszyć. Poniżej jego wersja (z minimalną modyfikacją dotyczącej nazw plików):
import unittest, xmlrunner, json, requests, glob
def abstract_test(self, data, response):
self.assertEqual(response.status_code, data['assert']['statusCode'])
self.assertSetEqual(set(response.json().keys()), set(data['assert']['responseKeys']))
self.assertLessEqual(response.elapsed.total_seconds(), data['assert']['responseTime'])
test = lambda data: lambda self: abstract_test(self, data, requests.request(**data['request']))
globals().update({file_name: type(file_name, (unittest.TestCase, ), {name: test(data) for name, data in json.loads(open(file_name, 'r').read()).items()}) for file_name in glob.iglob("*.json")})
unittest.main(testRunner=xmlrunner.XMLTestRunner())
Nie będę już analizował zmian, gdyż pozostawię to dla Ciebie w ramach treningu.
A może Ty również pokusisz się o jakaś wersję tego kodu, np. rozbudowaną o jakieś dodatkowe sprawdzenia?
Backlinks:
Python
- Jeszcze mniejszy framework do testów w Pythonie
- glob - wyszukiwanie plików i katalogów,
- type - sprawdzenie typu obiektu lub dynamiczne ich tworzenie,
Najmniejszy framework do testów w Pythonie
> Do tego artykułu istnieje kontynuacja, ale zachęcam do przeczytania obu artykułów w kolejności, w jakiej były publikowane. Jeśli jednak znasz już ten artykuł, to kolejny zatytułowany jest jeszcze mniejszy framework do testów w pythonie i również zachęcam do jego lektury.
> Kontynuację zmagań z najmniejszym frameworkiem do testów, możesz odnaleźć w kolejnym artykule zatytułowanym jeszcze mniejszy framework do testów. Zapraszam do jego lektury.
Jak zacząć automatyzować testy?
Poznaj 12 pytań, które pomogą Ci rozpocząć proces automatyzacji testów. W zupełnym oderwaniu od języka programowania, frameworków do testów oraz technologi w jakiej napisana została aplikacja, którą będziesz testować. Całość opisana prostym i zrozumiałym językiem.
Pobieram darmowy poradnik