blog.qalabs.pl Open in urlscan Pro
104.21.83.27  Public Scan

URL: https://blog.qalabs.pl/pytest/pytest-pierwsze-kroki/
Submission: On August 10 via api from US — Scanned from PL

Form analysis 0 forms found in the DOM

Text Content

 * 
 * 
 * 
 * 
 * 


PYTEST - PIERWSZE KROKI

pytest python

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 systemie Windows 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
2
3
4


ID,AUTHOR,TITLE,PAGES,CREATED,UPDATED
101,David Kahn,The Codebreakers,1200,2018-03-15,2018-03-17
102,Donald Knuth,The Art of Computer Programming,3168,2018-03-15,2018-03-15
103,Matt Weisfeld,The Object-Oriented Thought Process,336,2018-03-15,2018-03-18


Wykonamy dla niego testy, które sprawdzą:

 1. Czy nazwy kolumn w nagłówku są zapisane wielkimi literami.
 2. Czy pierwsza kolumna w nagłówku to ID.
 3. Czy nagłówek zawiera kolumnę CREATED
 4. Czy nagłówek zawiera kolumnę UPDATED
 5. Czy każdy rekord zawiera dane dla wszystkich kolumn z nagłówka.
 6. 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
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


import pytest


def test_header_is_uppercase():
    """Check if column names in header are uppercase"""
    assert True


def test_header_starts_with_id():
    """Check if the first column in header is ID"""
    assert True


def test_header_has_column_created():
    """Check if header has column CREATED"""
    assert True


def test_header_has_column_updated():
    """Check if header has column UPDATED"""
    assert True


def test_record_matches_header():
    """Check if number of columns in each record matches header"""
    assert True


def test_record_first_field_is_number():
    """Check if the first value in each record is a number"""
    assert True


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
2
3
4
5
6
7
8


============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.4.2, py-1.5.2, pluggy-0.6.0
rootdir: pytest-parametrize-example, inifile:
collected 6 items

test_csv.py ......                                                  [100%]

========================== 6 passed in 0.20 seconds ===========================



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
2
3
4
5
6
7
8


import pytest


@pytest.fixture()
def csv_data():
    with open('book.csv') as f:
        data = f.read().split('\n')
    return data


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
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


def test_header_is_uppercase(csv_data):
    """Check if column names in header are uppercase"""
    header = csv_data[0]
    assert header == header.upper()


def test_header_starts_with_id(csv_data):
    """Check if the first column in header is ID"""
    header = csv_data[0]
    column_names = header.split(',')
    first_column_name = column_names[0]
    assert first_column_name == 'ID'


def test_header_has_column_created(csv_data):
    """Check if header has column CREATED"""
    header = csv_data[0]
    column_names = header.split(',')
    assert 'CREATED' in column_names


def test_header_has_column_updated(csv_data):
    """Check if header has column UPDATED"""
    header = csv_data[0]
    column_names = header.split(',')
    assert 'UPDATED' in column_names


def test_record_matches_header(csv_data):
    """Check if number of columns in each record matches header"""
    header = csv_data[0]
    column_names = header.split(',')
    header_columns_count = len(column_names)
    errors = []
    for record in csv_data[1:]:
        record_values = record.split(',')
        record_values_count = len(record_values)
        if record_values_count != header_columns_count:
            errors.append(record)
    assert not errors


def test_record_first_field_is_number(csv_data):
    """Check if the first value in each record is a number"""
    errors = []
    for record in csv_data[1:]:
        record_values = record.split(',')
        if not record_values[0].isdigit():
            errors.append(record)
    assert not errors


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23


import pytest


@pytest.fixture()
def csv_data():
    with open('book.csv') as f:
        data = f.read().split('\n')
    return data


@pytest.fixture()
def csv_header(csv_data):
    return csv_data[0]


@pytest.fixture()
def csv_records(csv_data):
    return csv_data[1:]


@pytest.fixture()
def column_names(csv_header):
    return csv_header.split(',')


Teraz każda funkcja testowa może użyć dokładnie tych danych, których potrzebuje,
przygotowanych przez dedykowaną fixture.

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


def test_header_is_uppercase(csv_header):
    """Check if column names in header are uppercase"""
    assert csv_header == csv_header.upper()


def test_header_starts_with_id(column_names):
    """Check if the first column in header is ID"""
    first_column_name = column_names[0]
    assert first_column_name == 'ID'


def test_header_has_column_created(column_names):
    """Check if header has column CREATED"""
    assert 'CREATED' in column_names


def test_header_has_column_updated(column_names):
    """Check if header has column UPDATED"""
    assert 'UPDATED' in column_names


def test_record_matches_header(column_names, csv_records):
    """Check if number of columns in each record matches header"""
    header_columns_count = len(column_names)
    errors = []
    for record in csv_records:
        record_values = record.split(',')
        record_values_count = len(record_values)
        if record_values_count != header_columns_count:
            errors.append(record)
    assert not errors


def test_record_first_field_is_number(csv_records):
    """Check if the first value in each record is a number"""
    errors = []
    for record in csv_records:
        record_values = record.split(',')
        if not record_values[0].isdigit():
            errors.append(record)
    assert not errors


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
2
3
4


ID,STREET,CITY,COUNTRY,CREATED,UPDATED
301,Park Avenue,New York,USA,2018-03-15,2018-03-16
302,Main Street,Boston,USA,2018-03-15,2018-03-17
303,Elm Parkway,Rogers,USA,2018-03-15,2018-03-17


customer.csv

1
2
3
4
5


ID,NAME,E-MAIL,PHONE,CREATED,UPDATED
201,John Doe,john.doe@example.com,123456789,2018-03-15,2018-03-15
202,Mary Smith,mary.smith@example.com,987654321,2018-03-15,2018-03-15
203,Linda Williams,linda.williams@example.com,NULL,2018-03-15,2018-03-15
204,Robert Brown,robert.brown@example.com,333666999,2018-03-15,2018-03-15


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
2
3
4
5


@pytest.fixture(params=['address.csv', 'book.csv', 'customer.csv'])
def csv_data(request):
    with open(request.param) as f:
        data = f.read().split('\n')
    return data


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25


============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.4.2, py-1.5.2, pluggy-0.6.0
rootdir: pytest-parametrize-example, inifile:
collected 18 items

test_csv.py::test_header_is_uppercase[address.csv] PASSED                [  5%]
test_csv.py::test_header_is_uppercase[book.csv] PASSED                   [ 11%]
test_csv.py::test_header_is_uppercase[customer.csv] PASSED               [ 16%]
test_csv.py::test_header_starts_with_id[address.csv] PASSED              [ 22%]
test_csv.py::test_header_starts_with_id[book.csv] PASSED                 [ 27%]
test_csv.py::test_header_starts_with_id[customer.csv] PASSED             [ 33%]
test_csv.py::test_header_has_column_created[address.csv] PASSED          [ 38%]
test_csv.py::test_header_has_column_created[book.csv] PASSED             [ 44%]
test_csv.py::test_header_has_column_created[customer.csv] PASSED         [ 50%]
test_csv.py::test_header_has_column_updated[address.csv] PASSED          [ 55%]
test_csv.py::test_header_has_column_updated[book.csv] PASSED             [ 61%]
test_csv.py::test_header_has_column_updated[customer.csv] PASSED         [ 66%]
test_csv.py::test_record_matches_header[address.csv] PASSED              [ 72%]
test_csv.py::test_record_matches_header[book.csv] PASSED                 [ 77%]
test_csv.py::test_record_matches_header[customer.csv] PASSED             [ 83%]
test_csv.py::test_record_first_field_is_number[address.csv] PASSED       [ 88%]
test_csv.py::test_record_first_field_is_number[book.csv] PASSED          [ 94%]
test_csv.py::test_record_first_field_is_number[customer.csv] PASSED      [100%]

========================== 18 passed in 0.36 seconds ==========================



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
2
3
4


@pytest.mark.parametrize('checked_name', ['CREATED', 'UPDATED'])
def test_header_has_column(column_names, checked_name):
    """Check if header has column CREATED"""
    assert checked_name in column_names


Nie wpłynęło to na liczbę i charakter wykonanych testów, co można zauważyć w
szczegółowym wyniku:

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


============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.4.2, py-1.5.2, pluggy-0.6.0
rootdir: pytest-parametrize-example, inifile:
collected 18 items

test_csv.py::test_header_is_uppercase[address.csv] PASSED                [  5%]
test_csv.py::test_header_is_uppercase[book.csv] PASSED                   [ 11%]
test_csv.py::test_header_is_uppercase[customer.csv] PASSED               [ 16%]
test_csv.py::test_header_starts_with_id[address.csv] PASSED              [ 22%]
test_csv.py::test_header_starts_with_id[book.csv] PASSED                 [ 27%]
test_csv.py::test_header_starts_with_id[customer.csv] PASSED             [ 33%]
test_csv.py::test_header_has_column[address.csv-CREATED] PASSED          [ 38%]
test_csv.py::test_header_has_column[address.csv-UPDATED] PASSED          [ 44%]
test_csv.py::test_header_has_column[book.csv-CREATED] PASSED             [ 50%]
test_csv.py::test_header_has_column[book.csv-UPDATED] PASSED             [ 55%]
test_csv.py::test_header_has_column[customer.csv-CREATED] PASSED         [ 61%]
test_csv.py::test_header_has_column[customer.csv-UPDATED] PASSED         [ 66%]
test_csv.py::test_record_matches_header[address.csv] PASSED              [ 72%]
test_csv.py::test_record_matches_header[book.csv] PASSED                 [ 77%]
test_csv.py::test_record_matches_header[customer.csv] PASSED             [ 83%]
test_csv.py::test_record_first_field_is_number[address.csv] PASSED       [ 88%]
test_csv.py::test_record_first_field_is_number[book.csv] PASSED          [ 94%]
test_csv.py::test_record_first_field_is_number[customer.csv] PASSED      [100%]

========================== 18 passed in 0.39 seconds ==========================



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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16


import os
import pytest


def pytest_generate_tests(metafunc):
    listing = os.listdir()
    csv_files = [item for item in listing if item.endswith('.csv')]
    if 'csv_file' in metafunc.fixturenames:
        metafunc.parametrize('csv_file', csv_files)


@pytest.fixture()
def csv_data(csv_file):
    with open(csv_file) as f:
        data = f.read().split('\n')
    return data


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.


Nowsze
Narzędzia - system kontroli wersji Git i emulator konsoli cmder
Starsze
JUnit 5 - Pierwsze kroki

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.


© 2023 QA Labs (https://qalabs.pl) - Powered by Hexo - Theme Jane - Subskrybuj
naszego bloga używając czytnika kanałów (np. Feedly):
Blog Archives Tags Gitlab