Jak použít Python framework pro testování big dat

Nezáleží na tom, jestli s testováním začínáte, nebo už máte pár úspěšných projektů za sebou - tipy pro zefektivnění práce s daty se hodí vždycky. Pohodlně se usaďte a přečtěte si: 

Jak jsme využili testovací Behaviour Driven Development (BDD) framework v praxi 

Co udělat před tím, než zajdete za vývojářem 

Jak přistoupit k analýze a plánu testování 

Jak na přípravu a nastavení testovacího frameworku 

K čemu využít Python 

Jaké jsou přínosy Python BDD frameworku

Tento text nemá ambici být detailním návodem pro začátečníky v automatizaci. Předpokládá určité znalosti - především Pythonu, jeho ekosystému a SQL. Tyto znalosti ale pro kariéru v testování doporučuji doplnit alespoň na základní úrovni v každém případě. V zájmu příčetné délky textu nemohu ani popisovat každý řádek kódu - k tomuto účelu můžete použít veřejnou verzi repozitáře. Testy půjdou spustit, ale nebudou fungovat správně, protože z pochopitelných důvodů nemohu poskytnout přístupy do BigQuery databáze. Také bylo nutné anonymizovat šablony SQL dotazů a feature soubory. Zbylý zdrojový kód nicméně zůstal beze změny. 

Pracuji jako tester v agilním týmu vyvíjejícím řešení pro sledování chování uživatelů na  webových stránkách či aplikacích automobilové firmy. Data o chování uživatelů jsou pak pro každý web ukládána do BigQuery.

Představte si situaci, kdy za týmem jednoho dne přijde product owner s tímto zadáním: Je třeba, aby všechny hodnoty v polích obsahující nějakou formu anonymního, ale unikátního ID návštěvníka, které jsou starší 390 dní, byly anonymizovány. Pro mě jako testera je to výborná příležitost využít automatizaci.

Bez analýzy to nejde

Agilní vývoj nerovná se žádná analýza. Nemáte na ni týden, ale určitě stojí za to se v klidu zamyslet - můžete si tím totiž ušetřit spoustu problémů a nervů do budoucna. 

Než tedy naběhnete na kolegu vývojáře, který bude anonymizační řešení vymýšlet a implementovat, vyplatí se uvědomit si následující: 

  • Znám strukturu dat v BigQuery 
  • Databázové schéma určuje, že jsou data rozdělena do jednotlivých datasetů dle zemí a v rámci těchto datasetů se vygenerovaná data na webových stránkách ukládají do separátních denních tabulek (tzv. partition tables) 
  • Dat je obrovské množství a z hlediska k času a nákladů zkrátka není možné otestovat vše 
  • Ideální by bylo implementovat takové testovací řešení, které bude nezávislé na anonymizačním postupu zvoleném vývojářem 
  • Vzhledem k velkému množství dat by bylo ideální testování automatizovat 
  • Protože se budu dotazovat na stejná data pro různé dny a země, bude výhodné testy parametrizovat 

Vzhůru za vývojářem

S výše nastíněnou představou se už můžu vydat za vývojářem. Hlavní věcí, která mě zajímá je to, jak často anonymizační job poběží (denně, týdně, měsíčně), a jaká je přesná hranice, od které budou data anonymizována. Protože v danou chvíli ještě vývojář zvažuje, které technické řešení bude pro anonymizaci dat nejlepší, je vhodné vymyslet takový test, který bude na jeho rozhodnutí a implementované technologii nezávislý

V tuto chvíli už mám upřesněno, že: 

  • Budu testovat tzv. boundary values okolo data, od kterého jsou údaje anonymizovány - právě v těchto hodnotách se zpravidla nejčastěji objevují chyby 
  • Anonymizační job poběží denně 
  • Hraniční hodnotu, od které musí být data anonymizována, mohu specifikovat jako t - 391, kde t je den spuštění testu 
  • Veškerá práce s daty proběhne na straně serveru - tedy BigQuery 
  • Automatizační framework na straně klienta bude řídit běh testů, parametrizovat je, vyhodnocovat výsledky testů a generovat reporty 
  • Na BigQuery se lze bez problémů připojit přes API 

Základní představu a analýzu tedy mám a na tom už mohu stavět. Pokud se vám zdá, že to není příliš detailní, tak máte pravdu. Výše uvedený proces reálně zabral tak dvě hodiny čistého času. Beru totiž v úvahu, že iterace jsou nedílnou součástí nejen vývojového, ale i testovacího procesu v agilu :). 

Plán testování 

Pojďme si shrnout celkové nastavení testování ještě předtím, než se pustím do technického řešení automatizace. Známe hraniční hodnoty, které se budou testovat a frekvenci běhu anonymizačního jobu. 

Testovat se budou tato data: 

  • kontrola, že data nebyla anonymizována 

         - t - 1, t - 150, t - 360, t - 388, t - 389, t - 390 

  • kontrola, že data byla anonymizována 

         - t - 391, t - 392, t - [počet dní odkazující na nejstarší partition table] 

Testy se budou spouštět několik dní, aby bylo ověřeno, že anonymizační job pracuje konzistentně

Příprava testovacího frameworku 

Co se bude dít v BigQuery 

Protože nemohu uvádět konkrétní názvy polí a schéma databáze, bude část týkající se BigQuery pracovat se zobecněnými příklady

Schéma dat v BigQuery je následující: 

<project-id>.<dataset-id>.<table-prefix>* 

Pod project_id si můžeme představit celkové zastřešení dat v BigQuery. Project_id je ve schématu pro daný projekt z logiky věci jenom jedno. Datasety slouží k lepší organizaci dat a je na správci či architektovi databáze, aby zvážil, dle jakého klíče budou vytvářeny. V případě tohoto projektu sdružoval každý dataset veškerá data za jednu danou konkrétní zemi. Tabulky pak slouží k ukládání konkrétních dat. Kvůli zajištění co nejlepšího výkonu při zpracování dat se používají tzv. partition_tables, a v názvu se liší pouze tzv. suffixem, který je tvořen datem, například „2020-01-30“. Podrobně si můžete přečíst o syntaxi v manuálu

Prakticky je úkolem testu ověřit, že data před hraničním datem t - 391 nejsou anonymizována a data po hraničním datu t - 391 včetně anonymizována jsou. Z toho plyne, že potřebujeme dva SQL selecty - jeden pro dotazování dat před hraničním datem a druhý pro dotazování dat po hraničním datu. 

Logika obou dotazů je následující: 

  • Data před hraničním datem - pokud všechny údaje v polích nejsou anonymizovány, SQL select nevrátí nic 
  • Data po hraničním datu - pokud všechny údaje v polích jsou anonymizovány, SQL select nevrátí nic 

Dotazy jsou napsány tak, aby vrátily hodnoty, které (ne)odpovídají regexům, podle kterých jsou generovány hodnoty v id polích. Hodnoty se drží pevných pravidel formátování, a proto lze tento přístup bezpečně použít.  

SQL select pro data před hraničním datem 

Detailní vysvětlení selectu je mimo rozsah tohoto textu. Všimněte si jenom dvou placeholderů, které jsou použity pro parametrizaci testů - a to  “{dataset}” a “{date_}”. Kdyby vás zajímalo, jak se skriptuje v BigQuery, vše je detailně popsáno v dokumentaci

BEGIN

DECLARE r_client_agent_Id STRING;
DECLARE r_fullVisitorId STRING;
DECLARE r_default_client_Id STRING;

SET r_client_agent_Id = "^([a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12})$";
SET r_default_client_Id = "^([0-9]*\\.[0-9]*)$";
SET r_fullVisitorId = "^([0-9]{5,100})$";

SELECT
  clientId,
  fullVisitorId,
  agentId,
  date
FROM

  (SELECT
      clientId,
      fullVisitorId,
      (SELECT MAX(IF(index=1, value, NULL)) FROM UNNEST (hits.customDimensions)) AS agentId,
      date
  FROM `data.{dataset}.ga_sessions_*`, UNNEST (hits) AS hits
  WHERE _TABLE_SUFFIX = "{date_}"
  GROUP BY clientId, fullVisitorId, agentId, date
  )
WHERE
    ((REGEXP_CONTAINS(clientId, r_client_agent_Id) IS NOT TRUE AND REGEXP_CONTAINS(clientId, r_default_client_Id) IS NOT TRUE) OR
    REGEXP_CONTAINS(fullVisitorId, r_fullVisitorId) IS NOT TRUE OR
    (REGEXP_CONTAINS(agentId, r_client_agent_Id) IS NOT TRUE AND agentId IS NOT NULL)
    );
END;

SQL select pro data za hraničním datem 

Samotný SELECT je téměř totožný. Všimněte si jenom rozdílu v podmínce WHERE, kde filtrujeme data dle toho, zda splňují, či nikoliv, podmínku danou regulárním výrazem. 

BEGIN

DECLARE r_client_agent_Id STRING;
DECLARE r_fullVisitorId STRING;
DECLARE r_default_client_Id STRING;

SET r_client_agent_Id = "^([a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12})$";
SET r_default_client_Id = "^([0-9]*\\.[0-9]*)$";
SET r_fullVisitorId = "^([0-9]{5,100})$";

SELECT
  clientId,
  fullVisitorId,
  agentId,
  date
FROM

  (SELECT
      clientId,
      fullVisitorId,
      (SELECT MAX(IF(index=1, value, NULL)) FROM UNNEST (hits.customDimensions)) AS agentId,
      date
  FROM `data.{dataset}.ga_sessions_*`, UNNEST (hits) AS hits
  WHERE _TABLE_SUFFIX = "{date_}"
  GROUP BY clientId, fullVisitorId, agentId, date
  )
WHERE
    ((REGEXP_CONTAINS(clientId, r_client_agent_Id) IS TRUE AND REGEXP_CONTAINS(clientId, r_default_client_Id) IS TRUE) OR
    REGEXP_CONTAINS(fullVisitorId, r_fullVisitorId) IS TRUE OR
    REGEXP_CONTAINS(agentId, r_client_agent_Id) IS TRUE);
END;  

Nastavení testovacího frameworku 

Na základě minulých zkušeností jsem se rozhodl pro behave testovací framework. Je postavený na Pythonu a uplatňuje Behaviour-driven development (BDD) přístup k tvorbě testů. 

Prakticky to znamená, že při psaní automatizovaných testů oddělujeme soubory, které specifikují testy od těch, které je prostřednictvím kódu „pohání“. Soubory, které popisují testy, se označují jako „feature files“ a soubory s kódy napsanými v Pythonu jsou „step files“

Konkrétní feature soubor pro případ našeho testování pak může vypadat takto: 

@set1
Feature: Test Anonymization of 1st set of datasets

    Tests, whether anonymization of ID fields
    of BQ datasets were successful.

    @set1.notanon
    Scenario Outline: Data before treshold are not anonymized

        Tests, whether data before anonymization treshold
        (t - 391 days) are not anonymized.

        Given I get data for "<dataset>" dataset "not-anonymized" table in timepoint t-"<timedelta_>"
        When I count rows
        Then data were not anonymized if count is zero

        Examples:BigQuery datasets

            | dataset   | timedelta_  |
            # some_country
            | dataset_id | 1          |
            | dataset_id | 150        |
            | dataset_id | 365        |
            | dataset_id | 388        |
            | dataset_id | 389        |
            | dataset_id | 390        |

    @set1.anon
    Scenario Outline: Data after treshold are anonymized

        Tests, whether data after anonymization treshold
        (t - 391 days) are anonymized.

        Given I get data for "<dataset>" dataset "anonymized" table in timepoint t-"<timedelta_>"
        When I count rows
        Then data were anonymized if count is zero

        Examples:BigQuery datasets
            | dataset   | timedelta_   |
            # some_country
            | dataset_id | 391         |
            | dataset_id | 392         |
            | dataset_id | 1085        |

Když se podíváte na obsah feature souboru, jsou první výhody BDD přístupu a tohoto konkrétního frameworku zřejmé. Za prvé, test je samopopisný, takže i netechnickému čtenáři by mělo být celkem jasné, co test dělá. Za druhé - parametrizace je velice snadná: použitím Examples tabulky si určím parametry a framework vykoná test pro každý řádek parametrů automaticky. Za třetí - testy jsou psané s využitím takzvané Gherkinovy syntaxe. Díky tomu lze testy psát poměrně obecně a případné změny ve specifikaci testovaného softwaru se promítnou pouze do step souborů - tedy kódu, nikoliv samotných testů. 

Jak jsem uvedl, feature soubory, které může klidně vytvořit i neprogramátor, pak „rozpohybuje“ Python. Pojďme si nyní ukázat, jak to může vypadat. V tomto konkrétním případě stačilo rozhýbat „Given“, „When“ a „Then“ věty. O vše ostatní se postará samotný framework. 

Pohled pod kapotu motoru značky Python 

Takzvané step soubory se v behave frameworku používají ke skriptování testů, které jsou specifikovány ve feature souborech. Protože tento automatizovaný test byl z hlediska implementace poměrně jednoduchý, stačilo vytvořit jeden soubor. U složitějších řešení jich může být pochopitelně více. V takovém případě je pak třeba se zamyslet nad strukturou testovacího projektu, jmennou konvencí souborů, atp. A jenom připomínám možnost podívat se na celé řešení ve veřejném repozitáři

Behave řeší propojení Python kódu s větami z feature souborů tak, že jednotlivé věty se umisťují jako parametry tzv. dekorátorů funkcí. Parametr, neboli ona věta, musí být unikátní, nelze ji použít dvakrát pro různou funkci. Parametrizace testu probíhá přes vkládání proměnných do hodnoty textového řetězce, kterou framework automaticky parsuje. 

Pokud se podíváme na obsah tabulky feature souboru, tak pro každý řádek framework „dosadí“ do proměnných funkce hodnoty z tabulky a tímto způsobem se parametrizuje test. 

Všimněte si také, že všechny funkce se jmenují stejně - normálně to není možné a jakýkoliv linter vám vyhodí chyby, nicméně toto je implementační záměr frameworku. 

Dobrým zvykem je snažit se věty s parametry ve feature souborech formulovat tak, aby bylo možné je obsluhovat jednou funkcí - pomáhá to udržovat suchý kód (DRY - Do not Repeat Yourself). 

Velice důležitým parametrem je „context“ - jde o objekt frameworku, který mimo jiné umožňuje přenášet hodnoty mezi funkcemi obsluhujícími jednotlivé věty.

"""
importujeme nutné externí balíčky 

 - behave - balíček behave frameworku 
 - hamcrest - balíček pro testování assertů 
 - helpers.functions - sbírka vlastních funkcí, které pro přehlednost držíme mimo tento soubor
"""
from behave import given, then, when
from hamcrest import assert_that, empty, equal_to, is_, is_not

from helpers import functions as f


"""
Tato funkce získá parametry tabulky feature souboru, doplní je do SQL template, přes API  
se dotáže BigQuery a vrátí výsledek.  
Jak vidíte, ukládá pro použití ve funkcích obsluhující další věty výsledek query a  
hodnoty parametrů, které se použijí při případném ukládání do XLSX souborů. 
"""


@given(
    'I get data for "{dataset}" dataset "{state}" table in timepoint t-"{timedelta_}"'
)
def step_impl(context, dataset, state, timedelta_):
    """
    implements python code logic for @given decorator
    - Given statement from feature files.
        :param context:object
        :param dataset:str
        :param state:str
        :param timedelta_:str
        :returns context:object
    
    Context object is behave framework object, which allows for storing and using variables
    across step implementation functions.
    """
    if state == "not-anonymized":
        results = f.run_bq_job(f.FILEPATH_SQL_NOTANON, dataset, timedelta_)
    elif state == "anonymized":
        results = f.run_bq_job(f.FILEPATH_SQL_ANON, dataset, timedelta_)

    context.results = results
    context.timedelta_ = timedelta_
    context.dataset = dataset

    assert_that(context.results, is_not(empty()))


"""
Tato funkce použije výsledky dotazu z BigQuery a ověří, zda je počet řádků roven nule. 
Pokud ano, nastaví hodnotu příznaku na true a uloží pro zpracování u další věty. 
Pokud ne, tak nastaví hodnotu příznaku na false, stáhne data z BigQuery (do té doby totiž  
stáhnuta nebyla, dotaz vrátil takzvaný iterátor, podrobnosti v dokumentaci API) a uloží je  
s unikátním jménem do XLSX souboru s časovou známkou. 
"""


@when("I count rows")
def step_impl(context):
    """
    implements python code logic for @when decorator
    - When statement from feature files.
        :param context:object
        :returns context:object
    
    Context object is behave framework object, which allows for storing and using variables
    across step implementation functions.
    """
    if context.results.total_rows == 0:
        context.flag = True
    else:
        context.flag = False
        df = f.convert_to_dataframe(context.results)
        f.save_data_to_excel(
            f.FILEPATH_BQ_DATA,
            df,
            context.dataset,
            context.timedelta_,
            context.scenario.name[:11],
        )
        f.add_html_attachment(
            f.render_df_html_snapshot(df), "First 10 rows of BQ data of failed test."
        )
        f.add_file_link_attachment(f.FILEPATH_BQ_DATA, "BQ data of failed tests")


"""
Tato funkce vezme hodnotu uloženého příznaku a assertem ověří, zda se má hodnotu 
True. Pokud ano, framework test vyhodnotí jako PASS, jinak jako FAIL. 
"""


@then("data {state} anonymized if count is zero")
def step_impl(context, state):
    """
    implements python code logic for @then decorator
    - Then statement from feature files.
        :param context:object
        :param state:str
        :returns context:object
    
    Context object is behave framework object, which allows for storing and using variables
    across step implementation functions.
    """
    if state in ("were", "were not"):
        assert_that(context.flag, is_(equal_to(True)))

Přínosy použití BDD frameworku 

V případě tohoto konkrétního byznysového zadání jsme těžili hned z několika výhod: 

  • Díky předchozí znalosti frameworku a BigQuery jsme dosáhli velice rychlého nastavení pro testování - vše bylo hotovo za cca 1 den sprintu, tedy zhruba za osm hodin. 
  • Datasetů je v databázi cca šedesát. Některé obsahují přes tisíc denních tabulek, každá může mít počty záznamů pohybující se na úrovni desítek až stovek tisíc řádků 
  •  Při testování pouze i jen hraničních hodnot by manuální testování obnášelo exekuci cca 9 dotazů pro každý dataset s nutností ručně upravovat parametry v dotazu do databáze. Namísto několikahodinové náročnosti každého testovacího cyklu trvá automatizovaná exekuce cca 10 minut. 
  • Zápis testů formou feature souborů může být součástí akceptačních kritérií. 
  • Exekuci testů lze provádět prostřednictvím CI serverů typu Jenkins. 
  • Automatizovaný test generuje interaktivní HTML report za pomoci Allure reportovacího nástroje (obecná demo ukázka reportu je k nalezení na stránkách týmu, který jej vyvíjí). 
  • V případě testů, které neprošly, uloží BigQuery data do excelovské tabulky, kterou lze poskytnout vývojáři při hledání chyby. 

Autor: Radek Bednařík

V testingu pracuji necelé dva roky a je to nejlepší job mého života. A protože už mám něco za sebou, tak věřím, že je to fér hodnocení. Prošel jsem akademickou sférou, ekonomickou novinařinou a PR. Nikdy mě moje práce tak nebavila jako teď. V testingu se zaměřuji na automatizaci s využitím Pythonu a JavaScriptu. Kromě testingu i něco tvořím, mrkněte na můj Github.


Chcete se taky učit a růst na projektech?

Přidejte se k nám. Právě se díváme například po integračním test analytikovi a nabízíme také další testerské pozice.

Zajímá vás, s jakou investicí při testování počítat?

Využijte naši kalkulačku testovacích nákladů.