Radek Bednařík
Senior Test Automation Engineer
Blog
Radek Bednařík
Senior Test Automation Engineer
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:
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.
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í:
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:
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 :).
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:
- t - 1, t - 150, t - 360, t - 388, t - 389, t - 390
- 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ě.
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í:
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í:
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.
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;
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;
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 "not-anonymized" table in timepoint t-"" 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 "anonymized" table in timepoint t-"" 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.
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)))
V případě tohoto konkrétního byznysového zadání jsme těžili hned z několika výhod: