Philip Withnall
philip.withnall@collabora.co.uk 2015
Návrh softwaru tak, aby mohl být testován, a psaní jednotkových testů pro něj

# Jednotkové testy

#

        Shrnutí

        Jednotkové testování by mělo být hlavním způsobem testování většiny napsaného kódu, protože jednotkové testy stačí napsat jednou, ale spouštět se mohou vícekrát  — ruční testy vymyslíte jednou, ale pak je musíte pokaždé ručně provést.

        Vývoj jednotkových testů začíná návrhem architektury a API kódu, který má být testován: kód by měl být navržen tak, aby byl snadno testovatelný, protože jinak může být jeho testování velmi obtížné.

          Pište jednotkové testy tak, aby byly co nejmenší možné, ale ne menší. ()
          Používejte k psaní testů nástroje pro pokrytí kódu, abyste kód pokryli co nejvíce. ()
          Všechny jednotkové testy spouštějte pod Vlagrindem, aby se kontrolovaly úniky a jiné problémy. ()
          Používejte příslušné nástroje pro automatické generování jednotkových testů všude, kde je to možné. ()
          Kód navrhujte už od začátku tak, aby byl testovatelný. ()

# Psaní jednotkových testů

Jednotkové testy by měly být psány v souladu s pohledem na informace o pokrytí kódu získané z běžících testů. To obvykle znamená napsaní nějaké počáteční sady jednotkových testů, jejich spuštění pro získání údajů o pokrytí, následné přepracování a jejich rozšíření, aby se zvýšila úroveň pokrytí. Pokrytí lze zvýšit za prvé snahou o pokrytí všech funkcí (nebo aspoň částečné pokrytí) a potom také snahou o pokrytí všech řádků kódu. Tím, že pokryjete nejdříve funkce, můžete rychle najít problémy v API, které se brání účinnému testování. Ty mají typicky podobu interních funkcí, které nelze jednoduše volat z jednotkových testů. Celkově se dá říci, že byste se měli zaměřit na úroveň pokrytí nad 90 %. Neomezujte testy jen na pokrytí věcí, které se vztahují k požadavkům na projekt – testujte vše.

Obdobně jako zařazení do gitu, i jednotkový test by měl být „co nejmenší je možné, ale ne menší“ a testovat jen jedno konkrétní API nebo chování. Každý test musí být spustitelný samostatně, bez závislosti na stavu od ostatních testů. To je důležité, aby bylo možné ladit selhání jednotlivých testů samostatně, bez nutnosti procházet celou sérii dalších testů. Díky tomu lze také snadněji vysledovat selhání ke konkrétnímu API, namísto obecné hlášky „jednotkový test někde selhal“.

GLib má podporu pro jednotkové testování pomocí jejího frameworku GTest, který umožňuje testy uspořádat do skupin a hierarchie. To znamená, že skupiny souvisejících testů se dají spouště také dohromady pro zdokonalené ladění. Stačí spustit testovací spustitelný soubor s argumentem -p: ./název-testovací-sady -p /cesta/k/testovací/skupině.

# Nainstalované testy

Všechny jednotkové testy by měly být nainstalovány celosystémově s dodržením standardů pro instalaci testů.

Instalací jednotkových testů se zjednoduší průběžná začleňování, protože testy pro jeden projekt je možné spustit znovu po změnách v jiném projektu v průběžně začleňovaném prostředí, a tím testovat rozhraní mezi moduly. To je užitečné hlavně pro hodně provázané sady projektů, jako je tomu v GNOME.

Když chcete přidat podporu pro instalované testy, přidejte do configure.ac následující:

# Installed tests AC_ARG_ENABLE([modular_tests], AS_HELP_STRING([--disable-modular-tests], [Disable build of test programs (default: no)]),, [enable_modular_tests=yes]) AC_ARG_ENABLE([installed_tests], AS_HELP_STRING([--enable-installed-tests], [Install test programs (default: no)]),, [enable_installed_tests=no]) AM_CONDITIONAL([BUILD_MODULAR_TESTS], [test "$enable_modular_tests" = "yes" || test "$enable_installed_tests" = "yes"]) AM_CONDITIONAL([BUILDOPT_INSTALL_TESTS],[test "$enable_installed_tests" = "yes"])

A potom do tests/Makefile.am:

insttestdir = $(libexecdir)/installed-tests/[project]

all_test_programs = \ test-program1 \ test-program2 \ test-program3 \ $(NULL) if BUILD_MODULAR_TESTS TESTS = $(all_test_programs) noinst_PROGRAMS = $(TESTS) endif

if BUILDOPT_INSTALL_TESTS insttest_PROGRAMS = $(all_test_programs)

testmetadir = $(datadir)/installed-tests/[project] testmeta_DATA = $(all_test_programs:=.test)

testdatadir = $(insttestdir) testdata_DATA = $(test_files)

testdata_SCRIPTS = $(test_script_files) endif

EXTRA_DIST = $(test_files)

%.test: % Makefile $(AM_V_GEN) (echo '[Test]' > $@.tmp; \ echo 'Type=session' >> $@.tmp; \ echo 'Exec=$(insttestdir)/$<' >> $@.tmp; \ mv $@.tmp $@)

# Kontrola úniků

Jakmile napíšete jednotkové testy pokrývající velkou část kódu, můžete je spouštět pod různými dynamickými analytickými nástroji, jako je Valgrind, abyste zkontrolovali úniky, chyby vláken, problémy s alokací apod. napříč celým zdrojovým kódem. Čím vyšší je pokrytí kódu jednotkovými testy, tím je možné výsledky z Valgrindu brát jako důvěryhodnější. Více informací viz , včetně návodu na integraci do sestavovacího systému.

Podstatné je, že jednotkové testy by neměly samy o sobě způsobovat uniky paměti nebo jiných prostředků a stejně tak způsobovat problémy s vlákny. Jakékoliv takové problémy by ve skutečnosti způsobovaly falešná pozitivní hlášení v analýzách reálného kódu projektu. (Falešná pozitivní hlášení, která musí být opravena opravou jednotkových testů.)

# Generování testů

Některé formy kódu se dost často opakují a vyžadují hodně jednotkových testů, aby byly dobře pokryty. Na druhou stranu jsou ale vhodné pro generování testovacích dat a na to existují nástroje, které pro kód automaticky vygenerují testovací vektory. Tím se výrazně sníží čas potřebný pro psaní jednotkových testů pro kód v této konkrétní oblasti.

## JSON

Jedním z příkladů domény, vůči níž se testuje generování dat, je jejich zpracování v případech, kdy se očekává dodržení striktního schématu dat. To je případ dokumentů XML a JSON. Pro JSON lze použít Walbottle pro vygenerování testovacích vektorů pro všechny typy platných i neplatných vstupů odpovídajících schématu.

Všechny typy dokumentů JSON by k tomu měly mít definováno schéma JSON, které můžete předat do Walbottle, aby vygeneroval testovací vektory:

json-schema-generate --valid-only schéma.json json-schema-generate --invalid-only schéma.json

Tyto testovací vektory je pak možné předat kódu právě testovanému v jeho jednotkových testech. Instance JSON vygenerované pomocí --valid-only by měly být přijaty, zatímco ty vygenerované pomocí --invalid-only odmítnuty.

# Psaní testovatelného kódu

Při psaní kódu by se mělo myslet na jeho testovatelnost už ve fázi návrhu, protože to zásadním způsobem ovlivňuje návrh API a architekturu. Zde je několik klíčových principů:

Nepoužívejte globální stavy. Unikátní objekty (singletony) jsou většinou špatný nápad, protože pro ně nelze vytvářet oddělené instance nebo je řídit v jednotkových testech.

Oddělte používání vnějších stavů, jako jsou databáze, sítě nebo souborové systémy. Jednotkové testy je pak mohou nahradit přístupem k vnějšímu stavu s falešným objektem. Společným přístupem k tomu je použít vnucování závislostí, které předají objekt obalující souborový systém kódu, který je právě testován. Například třída by neměla načítat globální databázi (z pevně daného místa v souborovém systému), protože jednotkové testy by pak potenciálně mohly přepsat běžící systémovou kopii databáze a nikdy by nemohly být spuštěny souběžně. Měly by být předáván objekt, který poskytne rozhraní k databázi. V produkčním systému to bude tenké obalení okolo databázového API, při testování by to pak mohl být pokusný objekt, který kontroluje požadavky, které mu jsou předávány a vrací pevně dané odpovědi pro různorodé testy.

Vystavte pomocné funkce, pokud mohou být obecně užitečné.

Rozdělte projekt do sady malých privátních knihoven, které pak propojíte dohromady minimálním množstvím spojovacího kódu, aby tak vznikl spustitelný celek. Každou pak můžete testovat odděleně.

# Externí odkazy

Tématem testovatelnosti softwaru se zabývají následující články:

Design for testability

Software testability

Vkládání závislostí

Software design for testing