Hlavní navigace

Testování aplikací naprogramovaných v jazyce Go

26. 3. 2019
Doba čtení: 36 minut

Sdílet

 Autor: Go Lang
Dnes se budeme věnovat problematice testování. Nejprve si ukážeme, jakým způsobem se vytváří a spouští standardní jednotkové testy (unit test) a následně se seznámíme s dalšími užitečnými nástroji: go-carpet a GoConvey.

Obsah

1. Testování aplikací naprogramovaných v jazyce Go

2. Vytvoření a spuštění jednotkového testu

3. Otestování funkcí a metod, které nejsou viditelné mimo svůj balíček

4. Výsledky testů v případě nesplnění nějaké testované podmínky

5. Okamžité ukončení jednotkového testu v případě detekce chyby

6. Jednotkové testy řízené tabulkami

7. Výběr testů pro spuštění na základě specifikovaného vzorku

8. Výběr testů na základě štítku (tagu)

9. Zjištění, které části programového kódu jsou pokryty jednotkovými testy

10. Nástroj go-carpet

11. Ukázka použití nástroje go-carpet

12. Nástroj GoConvey

13. Použití webového uživatelského rozhraní nástroje GoConvey

14. GoConvey a BDD testy

15. Mockování funkcí a metod pro potřeby jednotkových testů

16. Převody měn

17. Vytvoření mocku funkce get_exchange_rate_from_url

18. Repositář s demonstračními příklady

19. Odkazy na Internetu

1. Testování aplikací naprogramovaných v jazyce Go

Tvorba testů, ať již testů jednotkových, integračních, výkonnostních atd., je v současnosti prakticky nedílnou součástí vývoje nových informačních systémů. I z tohoto důvodu se v nabídce standardních nástrojů jazyka Go nachází i nástroj určený pro spouštění jednotkových testů (unit tests) s vyhodnocením jejich výsledků, zjištěním, která část zdrojových kódů je jednotkovými testy pokryta atd. V souvislosti s jednotkovými testy je mnohdy nutné určité části programu nahradit jejich zjednodušenými (umělými) variantami, které se nazývají mock. Nástroj či knihovnu pro mockování sice přímo v základní sadě nástrojů Go nenalezneme (lze ji doinstalovat), ovšem jak si ukážeme v navazujících kapitolách, je většinou možné si vystačit s možnostmi poskytovanými samotným programovacím jazykem (zejména se to týká využití rozhraní – interface).

Jak jsme si již řekli v úvodním odstavci, obsahuje standardní instalace programovacího jazyka Go i knihovnu určenou pro psaní jednotkových testů. Tato knihovna se jmenuje testing a základní informace o ní získáme stejným způsobem, jako je tomu v případě všech dalších knihoven či balíčků – příkazem go doc. Zde konkrétně následujícím způsobem:

$ go doc testing
 
package testing // import "testing"
 
Package testing provides support for automated testing of Go packages. It is
intended to be used in concert with the ``go test'' command, which automates
execution of any function of the form
 
    func TestXxx(*testing.T)
 
where Xxx does not start with a lowercase letter. The function name serves
to identify the test routine.
...
...
...

Samotná implementace jednotkových testů je představována běžnými funkcemi, jejichž jména začínají na Test a akceptují parametr typu *testing.T, tj. ukazatel na strukturu obsahující informace o kontextu, ve kterém jsou jednotlivé testy spouštěny:

type T struct {
        common // další struktura s informacemi o času spuštění testu atd. atd.
        isParallel bool
        context    *testContext // For running tests and subtests.
}

Důležité je, že existuje množství metod pro strukturu testing.T, které jsou použity právě při tvorbě jednotkových testů. Jedná se především o tyto metody:

# Metoda Stručný popis metody
1 Error provede se zalogování chyby a funkce s testem se označí příznakem „chyba“
2 Fail funkce s testem se označí příznakem „chyba“
3 FailNow dtto, ovšem současně se příslušná funkce i ukončí
4 Log zalogování zprávy, typicky s informací o chybě
5 Fatal odpovídá kombinaci volání funkcí Log+FailNow
Poznámka: typicky tedy ve funkcích TestXXX nalezneme volání metody Error nebo Fatal, podle toho, zda se má celá testovací funkce ukončit či nikoli.

2. Vytvoření a spuštění jednotkového testu

Již v dokumentaci je zmíněno, jakým způsobem se mají jednotkové testy tvořit, ovšem jedná se o tak důležité téma, že se mu budeme věnovat podrobněji v první polovině dnešního článku. Nejprve se podívejme na zdrojový kód obsahující funkci nazvanou Add, kterou budeme chtít otestovat. Kód funkce Add i příslušné funkce main je uložen v souboru pojmenovaném „add.go“:

package main
 
func Add(x int, y int) int {
        return x + y
}
 
func main() {
        println(Add(1, 2))
}

Jakým způsobem se napíše jednotkový test či jednotkové testy pro tuto funkci? Testy budou zapisovány do souboru pojmenovaného „add_test.go“, protože právě na základě řetězce „_test“ ve jménu souboru nástroje jazyka Go rozpoznávají, jestli se jedná o zdrojový kód, který má být součástí výsledné aplikace, či naopak o kód používaný pro testování.

Poznámka: ve skutečnosti existují i další možnosti, jakými je možné rozdělit zdrojové kódy a testy. Někteří programátoři dávají přednost tomu, aby byly soubory s jednotkovými testy uloženy ve zvláštním (pod)adresáři. To je možné, ovšem v dnešním článku pro jednoduchost použijeme standardní postup – rozlišení běžných zdrojových kódů od testů na základě řetězce „_test“, který se ve jméně zdrojových souborů uvádí vždy před koncovku „.go“.

Ukažme si tedy způsob naprogramování velmi jednoduchého jednotkového testu pro otestování funkcionality funkce Add. Použijeme přitom metodu , při jejímž zavolání se metodou Error zaregistruje, že test nebyl dokončen úspěšně:

package main
 
import "testing"
 
func TestAdd(t *testing.T) {
        result := Add(1, 2)
        if result != 3 {
                t.Error("1+2 should be 3, got ", result, "instead")
        }
}
Poznámka: připomeňme si, že v programovacím jazyku Go není podporován příkaz assert. Autory Go k tomu vedlo několik důvodů, které jsou shrnuty ve FAQ https://golang.org/doc/faq#as­sertions. Pro nás je v tuto chvíli důležité, že můžeme snadno řídit, zda se má po nesplnění nějaké podmínky celý test ukončit, či zda se má pouze zaznamenat chyba a testování bude pokračovat dále. Nevýhodou je, že (bez použití dalších pomocných knihoven) se v testech bude opakovat explicitní zápis podmínek tvořených strukturovaným příkazem if.

Pro spuštění jednotkových testů se nepoužívá příkaz go run, ale příkaz go test. Ten nalezne všechny soubory *_test.go v daném adresáři či podadresářích a pokusí se v něm spustit všechny funkce s implementací jednotkových testů:

$ go test
 
PASS
ok      _/home/tester/go-root/article_17/tests01   0.005s

Lepší je však použít přepínač -v, aby se vypsaly podrobnější informace o spuštěných testech:

$ go test -v
 
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests01   0.004s

3. Otestování funkcí a metod, které nejsou viditelné mimo svůj balíček

V předchozím demonstračním příkladu jsme vytvořili jednotkový test pro funkci viditelnou mimo svůj balíček, protože její jméno začíná velkým písmenem. Můžeme si ovšem ověřit, že otestovat můžeme i interní funkce volatelné pouze v rámci svého balíčku. Podívejme se na zdrojový kód druhého příkladu. Ten se od předchozího příkladu odlišuje pouze změnou názvu funkce pro součet dvou celočíselných hodnot:

package main
 
func add(x int, y int) int {
        return x + y
}
 
func main() {
        println(add(1, 2))
}

Jednotkový test pro funkci add bude vypadat prakticky stejně jako test pro funkci Add:

package main
 
import "testing"
 
func TestAdd(t *testing.T) {
        result := add(1, 2)
        if result != 3 {
                t.Error("1+2 should be 3, got ", result, "instead")
        }
}

Po spuštění jednotkových testů zjistíme, že je funkce add prakticky bez problémů testovatelná:

$ go test -v
 
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests02   0.006s
Poznámka: zde jsme si situaci značně zjednodušili tím, že testy jsou vlastně součástí stejného balíčku jako samotná testovaná funkce. Vyzkoušejte si, jak/zda se chování změní ve chvíli, kdy se použijí odlišné balíčky, například adder a adder_test.

4. Výsledky testů v případě nesplnění nějaké testované podmínky

Samozřejmě si můžeme vyzkoušet, jak se bude systém chovat v případě, že nějaká podmínka zapsaná v testech nebude splněna. Můžeme například naši testovanou funkci add nepatrně upravit takovým způsobem, aby dávala pro některé hodnoty špatné výsledky:

package main
 
func add(x int, y int) int {
        return x - y
}
 
func main() {
        println(add(1, 2))
}

Jednotkové testy budou vypadat následovně. Povšimněte si, že jsme přidali nový test pro zjištění, jak se funkce add chová ve chvíli, kdy je druhý operand nulový:

package main
 
import "testing"
 
func TestAdd(t *testing.T) {
        result := add(1, 2)
        if result != 3 {
                t.Error("1+2 should be 3, got ", result, "instead")
        }
 
        result = add(10, 20)
        if result != 30 {
                t.Error("10+20 should be 30, got ", result, "instead")
        }
}
 
func TestAddZero(t *testing.T) {
        result := add(1, 0)
        if result != 1 {
                t.Error("1+0 should be 1, got ", result, "instead")
        }
}

Jednotkové testy spustíme s přepínačem -v:

$ go test -v
 
=== RUN   TestAdd
--- FAIL: TestAdd (0.00s)
    add_test.go:8: 1+2 should be 3, got  -1 instead
    add_test.go:13: 10+20 should be 30, got  -10 instead
=== RUN   TestAddZero
--- PASS: TestAddZero (0.00s)
FAIL
exit status 1
FAIL    _/home/tester/go-root/article_17/tests03   0.005s
Poznámka: povšimněte si, že test TestAddZero byl spuštěn nezávisle na tom, jak dopadl předchozí test TestAdd. A ve vykonávání testu TestAdd se pokračovalo i tehdy, kdy došlo k první chybě.

5. Okamžité ukončení jednotkového testu v případě detekce chyby

V případě, že se má nějaký test (reprezentovaný jednou funkcí s názvem ve formátu TestXXX) ukončit ihned po detekci první chyby, je nutné namísto volání metody Error:

t.Error("...")

Zavolat buď dvojici metod Log+FailNow:

t.Log("...)
t.FailNow()

Popř. (což je kratší) metodu Fatal:

t.Fatal("...)

Ukažme si nyní úpravu jednotkových testů takovým způsobem, aby se ihned po nalezení prvního špatného výsledku test ukončil. Zdrojový kód takto upravených testů najdete na adrese https://github.com/tisnik/go-root/blob/master/article17/tes­ts04/add_test.go:

package main
 
import "testing"
 
func TestAdd(t *testing.T) {
        result := add(1, 2)
        if result != 3 {
                t.Log("1+2 should be 3, got ", result, "instead")
                t.FailNow()
        }
 
        result = add(10, 20)
        if result != 30 {
                t.Log("10+20 should be 30, got ", result, "instead")
                t.FailNow()
        }
}
 
func TestAddZero(t *testing.T) {
        result := add(1, 0)
        if result != 1 {
                t.Log("1+0 should be 1, got ", result, "instead")
                t.FailNow()
        }
}

Nové chování testů je následující:

$ go test -v
 
=== RUN   TestAdd
--- FAIL: TestAdd (0.00s)
    add_test.go:8: 1+2 should be 3, got  -1 instead
=== RUN   TestAddZero
--- PASS: TestAddZero (0.00s)
FAIL
exit status 1
FAIL    _/home/tester/go-root/article_17/tests04   0.004s
Poznámka: v původních testech se navíc testoval výsledek funkce add(10, 20).

6. Jednotkové testy řízené tabulkami

Velmi často se při psaní jednotkových testů v programovacím jazyku Go setkáme s tím, že se specifikuje sada vstupních dat pro testovanou funkci společně se sadou očekávaných hodnot. Tyto údaje můžeme reprezentovat různým způsobem, například je mít uložené v externích souborech (CSV, textové soubory, JSON atd. atd.), ovšem mnohdy si vystačíme s tím nejjednodušším a nejpřímějším řešením – zápisem dat ve formě pole. Typicky se jedná o pole struktur (záznamů), přičemž každý záznam obsahuje jak zmíněné vstupní hodnoty testované funkce, tak i její očekávaný výsledek či výsledky. V našem konkrétním případě budeme opět testovat funkci add, nyní ovšem již s explicitní specifikací přesného typu parametrů i jejich výsledků (předtím jsme se spokojili s použitím datového typu int, nyní použijeme konkrétní typ int32):

Zdrojový kód testovaného modulu:

package main
 
func add(x int32, y int32) int32 {
        return x + y
}
 
func main() {
        println(add(1, 2))
}

Jednotkové testy používající tabulku se vstupními hodnotami i hodnotami očekávanými (výsledky) mohou v tom nejjednodušším případě vypadat takto:

package main
 
import (
        "fmt"
        "math"
        "testing"
)
 
type AddTest struct {
        x        int32
        y        int32
        expected int32
}
 
func TestAdd(t *testing.T) {
        var addTestInput = []AddTest{
                {0, 0, 0},
                {1, 0, 1},
                {2, 0, 2},
                {2, 1, 3},
                {2, -2, 0},
                {math.MaxInt32, 0, math.MaxInt32},
                {math.MaxInt32, 1, math.MinInt32},
                {math.MaxInt32, math.MinInt32, -1},
        }
 
        for _, i := range addTestInput {
                result := add(i.x, i.y)
                if result != i.expected {
                        msg := fmt.Sprintf("%d + %d should be %d, got %d instead",
                                i.x, i.y, i.expected, result)
                        t.Error(msg)
                }
        }
}
Poznámka: použití dat zapsaných v poli struktur/záznamů je z pohledu autorů Go spolehlivým řešením, protože není nutné implementovat čtečku dalšího souborového formátu, základní rozsah a typ dat zkontroluje samotný překladač Go a při zápisu lze využít asistence programátorského textového editoru či integrovaného vývojového prostředí (kontextová nápověda, automatické doplňování kódu atd. atd.).

Výsledky testů:

$ go test -v
 
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests05   0.005s

Samozřejmě se můžeme podívat na to, jak se změní výsledky jednotkových testů ve chvíli, kdy bude funkce add implementována nekorektně:

$ go test -v
 
=== RUN   TestAdd
--- FAIL: TestAdd (0.00s)
    add_test.go:32: 2 + 1 should be 3, got 1 instead
    add_test.go:32: 2 + -2 should be 0, got 4 instead
    add_test.go:32: 2147483647 + 1 should be -2147483648, got 2147483646 instead
FAIL
exit status 1
FAIL    _/home/tester/go-root/article_17/tests06   0.004s

7. Výběr testů pro spuštění na základě specifikovaného vzorku

V této kapitole se seznámíme s jedním ze způsobů volby, které jednotkové testy mají být spuštěny. V některých situacích totiž potřebujeme, aby se spustila pouze určitá sada jednotkových testů. Příkladem může být rychlý test (někdy se zde dosti nepřesně používá termín smoke test) aplikace přímo z integrovaného vývojového prostředí, jenž může být díky rychlosti překladače jazyka Go spuštěn při každém uložení zdrojového souboru (stačí si v programátorském textovém editoru či v integrovaném vývojovém prostředí nastavit příslušnou událost, což je například v případě textového editoru Vim docela snadné s využitím události BufWritePost popř. dokonce BufLeave).

V takovém případě většinou nepotřebujeme spouštět všechny testy, ale jen jejich podmnožinu. Pokud při pojmenování funkcí s implementací jednotkových testů udržujeme nějakou konzistenci v pojmenování, je volba jen některých testů pro spuštění relativně snadná, protože můžeme použít příkaz:

$ go test -v -run vzorek/pattern

Nyní si ukažme, jak se tento příkaz použije v praxi. Jednotkové testy funkce add rozdělíme do několika samostatných funkcí – základní testy, testy se „zajímavými“ (krajními) hodnotami atd. Výsledek může vypadat takto:

package main
 
import (
        "fmt"
        "math"
        "testing"
)
 
type AddTest struct {
        x        int32
        y        int32
        expected int32
}
 
func checkAdd(t *testing.T, testInputs []AddTest) {
        for _, i := range testInputs {
                result := add(i.x, i.y)
                if result != i.expected {
                        msg := fmt.Sprintf("%d + %d should be %d, got %d instead",
                                i.x, i.y, i.expected, result)
                        t.Error(msg)
                }
        }
}
 
func TestAddBasicValues(t *testing.T) {
        var addTestInput = []AddTest{
                {0, 0, 0},
                {1, 0, 1},
                {2, 0, 2},
                {2, 1, 3},
        }
        checkAdd(t, addTestInput)
}
 
func TestAddNegativeValues(t *testing.T) {
        var addTestInput = []AddTest{
                {0, 0, 0},
                {1, 0, 1},
                {2, 0, 2},
                {2, 1, 3},
                {2, -2, 0},
        }
        checkAdd(t, addTestInput)
}
 
func TestAddMinValues(t *testing.T) {
        var addTestInput = []AddTest{
                {math.MinInt32, 0, math.MinInt32},
                {math.MinInt32, 1, math.MinInt32 + 1},
        }
        checkAdd(t, addTestInput)
}
 
func TestAddMaxValues(t *testing.T) {
        var addTestInput = []AddTest{
                {math.MaxInt32, 0, math.MaxInt32},
                {math.MaxInt32, 1, math.MinInt32},
                {math.MaxInt32, math.MinInt32, -1},
        }
        checkAdd(t, addTestInput)
}
 
func TestAddMinMaxValues(t *testing.T) {
        var addTestInput = []AddTest{
                {math.MinInt32, 0, math.MinInt32},
                {math.MinInt32, 1, math.MinInt32 + 1},
                {math.MaxInt32, 0, math.MaxInt32},
                {math.MaxInt32, 1, math.MinInt32},
                {math.MaxInt32, math.MinInt32, -1},
        }
        checkAdd(t, addTestInput)
}

Spuštění všech testů se provede nám známým příkazem bez dalších modifikací:

$ go test -v
 
=== RUN   TestAddBasicValues
--- PASS: TestAddBasicValues (0.00s)
=== RUN   TestAddNegativeValues
--- PASS: TestAddNegativeValues (0.00s)
=== RUN   TestAddMinValues
--- PASS: TestAddMinValues (0.00s)
=== RUN   TestAddMaxValues
--- PASS: TestAddMaxValues (0.00s)
=== RUN   TestAddMinMaxValues
--- PASS: TestAddMinMaxValues (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests07   0.005s

Spuštění těch testů, které v názvu používají slovo „Max“:

$ go test -v -run Max
 
=== RUN   TestAddMaxValues
--- PASS: TestAddMaxValues (0.00s)
=== RUN   TestAddMinMaxValues
--- PASS: TestAddMinMaxValues (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests07   0.006s

Spuštění těch testů, které v názvu používají slovo „Min“:

$ go test -v -run Min
 
=== RUN   TestAddMinValues
--- PASS: TestAddMinValues (0.00s)
=== RUN   TestAddMinMaxValues
--- PASS: TestAddMinMaxValues (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests07   0.004s

V případě, že použijeme vzorek neodpovídající žádnému testu, budeme o tom informování varovnou zprávou:

$ go test -v -run FooBar
 
testing: warning: no tests to run
PASS
ok      _/home/tester/go-root/article_17/tests07   0.005s

8. Výběr testů na základě štítku (tagu)

Existuje ovšem ještě další způsob určení, jaké testy se mají spustit. V tomto případě ovšem nedochází k volbě skupiny funkcí s implementacemi jednotkových testů, ale celých souborů, v nichž jsou jednotkové testy uloženy. Předchozí příklad si tedy upravíme následujícím způsobem:

  • Zdrojový kód testované funkce bude uložen v souboru pojmenovaném „add.go“, tj. tak, jak jsme zvyklí.
  • Základní testy budou uloženy v souboru „add_test.go“, tj. opět se použije standardní pojmenování.
  • Ovšem navíc budou existovat i dva soubory „add_slow_test.go“ a „add_fast_test.go“ s dalšími (řekněme pomaleji a rychleji běžícími) testy.

Důležité je, že ve dvou posledně zmíněných souborech použijeme strukturovaný komentář, který je rozeznán a zpracováván překladačem programovacího jazyka Go:

// +build fast
 
// +build slow
 
atd.

Podívejme se nyní na obsahy jednotlivých souborů zmíněných v předchozím seznamu.

Soubor add_test.go:

package main
 
import "testing"
 
func TestAdd(t *testing.T) {
        result := add(1, 2)
        if result != 3 {
                t.Error("1+2 should be 3, got ", result, "instead")
        }
}

Soubor add_fast_test.go:

// +build fast
 
package main
 
import (
        "fmt"
        "testing"
)
 
type AddTest struct {
        x        int32
        y        int32
        expected int32
}
 
func checkAdd(t *testing.T, testInputs []AddTest) {
        for _, i := range testInputs {
                result := add(i.x, i.y)
                if result != i.expected {
                        msg := fmt.Sprintf("%d + %d should be %d, got %d instead",
                                i.x, i.y, i.expected, result)
                        t.Error(msg)
                }
        }
}
 
func TestAddBasicValues(t *testing.T) {
        var addTestInput = []AddTest{
                {0, 0, 0},
                {1, 0, 1},
                {2, 0, 2},
                {2, 1, 3},
        }
        checkAdd(t, addTestInput)
}
 
func TestAddNegativeValues(t *testing.T) {
        var addTestInput = []AddTest{
                {0, 0, 0},
                {1, 0, 1},
                {2, 0, 2},
                {2, 1, 3},
                {2, -2, 0},
        }
        checkAdd(t, addTestInput)
}

Soubor add_slow_test.go:

// +build slow
 
package main
 
import (
        "fmt"
        "math"
        "testing"
)
 
type AddTest struct {
        x        int32
        y        int32
        expected int32
}
 
func checkAdd(t *testing.T, testInputs []AddTest) {
        for _, i := range testInputs {
                result := add(i.x, i.y)
                if result != i.expected {
                        msg := fmt.Sprintf("%d + %d should be %d, got %d instead",
                                i.x, i.y, i.expected, result)
                        t.Error(msg)
                }
        }
}
 
func TestAddMinValues(t *testing.T) {
        var addTestInput = []AddTest{
                {math.MinInt32, 0, math.MinInt32},
                {math.MinInt32, 1, math.MinInt32 + 1},
        }
        checkAdd(t, addTestInput)
}
 
func TestAddMaxValues(t *testing.T) {
        var addTestInput = []AddTest{
                {math.MaxInt32, 0, math.MaxInt32},
                {math.MaxInt32, 1, math.MinInt32},
                {math.MaxInt32, math.MinInt32, -1},
        }
        checkAdd(t, addTestInput)
}
 
func TestAddMinMaxValues(t *testing.T) {
        var addTestInput = []AddTest{
                {math.MinInt32, 0, math.MinInt32},
                {math.MinInt32, 1, math.MinInt32 + 1},
                {math.MaxInt32, 0, math.MaxInt32},
                {math.MaxInt32, 1, math.MinInt32},
                {math.MaxInt32, math.MinInt32, -1},
        }
        checkAdd(t, addTestInput)
}

Nyní si můžeme jednotlivé testy spustit. Povšimněte si použití přepínače -tags:

$ go test -v
 
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests08   0.003s
$ go test -v -tags fast
 
=== RUN   TestAddBasicValues
--- PASS: TestAddBasicValues (0.00s)
=== RUN   TestAddNegativeValues
--- PASS: TestAddNegativeValues (0.00s)
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests08   0.004s
$ go test -v -tags slow
 
=== RUN   TestAddMinValues
--- PASS: TestAddMinValues (0.00s)
=== RUN   TestAddMaxValues
--- PASS: TestAddMaxValues (0.00s)
=== RUN   TestAddMinMaxValues
--- PASS: TestAddMinMaxValues (0.00s)
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      _/home/tester/go-root/article_17/tests08   0.004s

9. Zjištění, které části programového kódu jsou pokryty jednotkovými testy

Další problematikou, s níž se v dnešním článku seznámíme, je zjištění, jaké části zdrojového kódu jsou vůbec pokryty jednotkovými testy. To je velmi užitečná informace, která nám umožní soustředit se na efektivní psaní jednotkových testů, hledání různých „corner cases“ atd. Již v základní sadě nástrojů jazyka Go máme možnost si informace o pokrytí testy zjistit, takže si nejprve vytvořme funkci, kterou musíme otestovat. Bude se jednat o klasickou funkci určenou pro výpočet faktoriálu („školní“ rekurzivní varianta):

package factorial
 
func Factorial(n int64) int64 {
        switch {
        case n < 0:
                return 1
        case n == 0:
                return 1
        default:
                return n * Factorial(n-1)
        }
}

Jednotkové testy pro tuto funkci mohou vypadat například následovně:

package factorial_test
 
import (
        "factorial"
        "testing"
)
 
func TestFactorialForZero(t *testing.T) {
        result := factorial.Factorial(0)
        if result != 1 {
                t.Errorf("Expected 0! == 1, but got %d instead", result)
        }
}
 
func TestFactorialForTen(t *testing.T) {
        result := factorial.Factorial(10)
        expected := int64(3628800)
        if result != expected {
                t.Errorf("Expected 0! == %d, but got %d instead", expected, result)
        }
}

Samotné spuštění testů by nemělo skončit s chybou:

$ go test -v
 
=== RUN   TestFactorialForZero
--- PASS: TestFactorialForZero (0.00s)
=== RUN   TestFactorialForTen
--- PASS: TestFactorialForTen (0.00s)
PASS
ok      _/home/tester/go-root/article_17/factorial 0.005s

Otázkou ovšem je, zda jsme skutečně napsali testy takovým způsobem, že pokryjí všechny větve zdrojového kódu. Proto spustíme jiný příkaz:

$ go test -v --cover
 
=== RUN   TestFactorialForZero
--- PASS: TestFactorialForZero (0.00s)
=== RUN   TestFactorialForTen
--- PASS: TestFactorialForTen (0.00s)
PASS
coverage: 75.0% of statements
ok      factorial       0.005s

Popř. si můžeme vygenerovat soubor s informacemi o pokrytí testy:

$ go test -v --coverprofile coverage

Výsledkem předchozího příkladu bude soubor obsahující informace o tom, kterými větvemi program prošel při svém testování. Obsah tohoto souboru by měl být přibližně následující:

mode: set
factorial/factorial.go:3.31,4.9 1 1
factorial/factorial.go:5.13,6.11 1 0
factorial/factorial.go:7.14,8.11 1 1
factorial/factorial.go:9.10,10.28 1 1

Následně z tohoto souboru vygenerujeme HTML stránku s podrobnějšími informacemi o jednotlivých testovaných řádcích:

$ go tool cover -html=cover.out -o cover.html

10. Nástroj go-carpet

Získání informace o tom, kterými větvemi programu se prošlo při testování, je však možné ještě více polidštit. Zde si již ovšem nevystačíme se standardními nástroji jazyka Go, ale budeme muset použít externí pomocný nástroj. Ten se jmenuje go-carpet a instaluje se naprosto stejným způsobem, jako jakýkoli jiný balíček jazyka Go, tj. příkazem go get:

$ go get github.com/msoap/go-carpet

Po instalaci tohoto nástroje můžeme použít nový příkaz go-carpet, ovšem za předpokladu, že máte na PATH umístěnou i cestu „~/go/bin“ (o tom jsme se zmínili již v předchozím článku). Pokud není proměnná PATH nastavena, můžete výše uvedený příkaz volat s celou cestou z adresáře, v němž se nachází jednotkové testy:

$ ~/go/bin go-carpet

11. Ukázka použití nástroje go-carpet

Výsledek je v tomto případě odlišný, protože bude vypsán zdrojový kód testované aplikace se zvýrazněním těch funkcí větví, které byly pokryty jednotkovými testy. Kvůli tomu, že odlišení je ve výchozím nastavení provedeno odlišnou barvou, ukážeme si screenshot terminálu s výsledky:

Obrázek 1: Screenshot terminálu se zvýrazněním těch částí zdrojového kódu, které byly pokryty jednotkovými testy. Ze screenshotu je patrné, že pro jednu větev je nutné napsat další test.

12. Nástroj GoConvey

V závěru článku se – prozatím pouze v rychlosti – musíme zmínit o velmi užitečné aplikaci nazvané GoConvey. Tato aplikace primárně umožňuje spouštět jednotkové testy, a to jak z příkazové řádky, tak i z webového uživatelského rozhraní.

Poznámka: pravděpodobně jste si již všimli, že jazyk Go je primárně používán pro psaní síťových aplikací, síťových služeb či právě webových aplikací. Existuje sice několik knihoven pro tvorbu aplikací s plnohodnotným grafickým uživatelským rozhraním, ovšem prozatím (a pravděpodobně se to nezmění) budou dominovat aplikace vybavené webovým rozhraním. A mezi tyto aplikace patří i zde popisovaný nástroj GoConvey.

To ovšem není zdaleka vše, protože může sloužit i pro psaní BDD testů, ovšem nikoli s využitím doménově specifického jazyka Gherkin, ale „pouze“ s využitím samotného programovacího jazyka Go doplněného o několik funkcí. Této zajímavé a užitečné problematice se budeme věnovat v navazujících kapitolách. Jak je ve světě jazyka Go zvykem, je instalace této aplikace snadná, protože se opět využívá příkazu go get:

$ go get github.com/smartystreets/goconvey

13. Použití webového uživatelského rozhraní nástroje GoConvey

Podívejme se nyní na způsob použití webového uživatelského rozhraní tohoto nástroje. Příkaz goconvey je nutné spustit v adresáři s jednotkovými testy:

$ cd ~/go/src/factorial/
 
$ ~/go/bin/goconvey 
 
2019/03/25 19:29:42 goconvey.go:63: Initial configuration: [host: 127.0.0.1] [port: 8080] [poll: 250ms] [cover: true]
2019/03/25 19:29:42 tester.go:19: Now configured to test 10 packages concurrently.
2019/03/25 19:29:42 goconvey.go:194: Serving HTTP at: http://127.0.0.1:8080
2019/03/25 19:29:42 goconvey.go:107: Launching browser on 127.0.0.1:8080
2019/03/25 19:29:42 integration.go:122: File system state modified, publishing current folders... 0 3106557094
2019/03/25 19:29:42 goconvey.go:120: Received request from watcher to execute tests...
2019/03/25 19:29:42 executor.go:69: Executor status: 'executing'
2019/03/25 19:29:42 coordinator.go:46: Executing concurrent tests: factorial
2019/03/25 19:29:42 goconvey.go:115:
2019/03/25 19:29:44 parser.go:24: [passed]: factorial
2019/03/25 19:29:44 executor.go:69: Executor status: 'idle'

Prakticky okamžitě by se v prohlížeči měla (automaticky) zobrazit webová stránka s rozhraním tohoto nástroje:

Obrázek 2: Webové rozhraní aplikace GoConvey.

Obrázek 3: Změna stylu webového rozhraní aplikace GoConvey.

Obrázek 4: Historie již spuštěných testů.

Obrázek 5: Výsledky v případě, že některé testy neproběhly korektně.

14. GoConvey a BDD testy

Při použití nástroje GoConvey je možné vytvářet i BDD testy, s nimiž jsme se seznámili v článku Behavior-driven development v Pythonu s využitím knihovny Behave. Ovšem v případě GoConvey se nepoužívá doménově specifický jazyk Gherkin – testy se zapisují přímo v jazyku Go, ovšem s použitím funkcí a metod z knihovny convey.

Použití BDD si opět ukážeme na implementaci výpočtu faktoriálu:

package factorial
 
func Factorial(n int64) int64 {
        switch {
        case n < 0:
                return 1
        case n == 0:
                return 1
        default:
                return n * Factorial(n-1)
        }
}

Samotné testy jsou uloženy v souboru behaviour_test.go:

package factorial
 
import(
        "testing"
        . "github.com/smartystreets/goconvey/convey"
)
 
func TestFactorial(t *testing.T) {
        Convey("0! should be equal 1", t, func() {
                So(Factorial(0), ShouldEqual, 1)
        })
}
 
func TestFactorial2(t *testing.T) {
        Convey("10! should be greater than 1", t, func() {
                So(Factorial(10), ShouldBeGreaterThan, 1)
        })
        Convey("10! should be between 1 and 10000000", t, func() {
                So(Factorial(10), ShouldBeBetween, 1, 10000000)
        })
}

Spuštění BDD testů je snadné, protože se použije nám již známý příkaz:

$ go test -v
 
=== RUN   TestFactorial
 
  0! should be equal 1 ✔
 
 
1 total assertion
 
--- PASS: TestFactorial (0.00s)
=== RUN   TestFactorial2
 
  10! should be greater than 1 ✔
 
 
2 total assertions
 
 
  10! should be between 1 and 10000000 ✔
 
 
3 total assertions
 
--- PASS: TestFactorial2 (0.00s)
PASS
ok      _/home/tester/go-root/article_17/factorial_convey  0.006s
Poznámka: podrobnější informace o tom, jakým způsobem se BDD testy vytvářejí a spouštějí, si řekneme příště. V dnešním článku se o nástroji GoConvey zmiňujeme především z toho důvodu, že je potenciálně velmi užitečný a přitom prozatím (relativně) neznámý.

15. Mockování funkcí a metod pro potřeby jednotkových testů

Při testování aplikací, zejména při psaní jednotkových testů, se poměrně často dostaneme do situace, kdy potřebujeme nahradit nějakou funkci či metodu používanou v reálné aplikaci za „falešnou“ funkci resp. metodu vytvořenou pouze pro účely testů. V programovacím jazyku Go je možné pro tvorbu a použití takových „falešných“ funkcí použít hned několik různých knihoven, které se od sebe odlišují jak svými možnostmi, tak i způsobem zápisu či deklarace očekávaného chování testované aplikace. Ovšem pro lepší pochopení celé problematiky si dnes ukážeme, jak lze mockování (většinou) provést pouze s využitím základních možností programovacího jazyka Go, zejména s použitím rozhraní.

16. Převody měn

Testovat budeme aplikaci, která bude provádět převod měn na základě kurzovního lístku. Samotný převod měny je triviální (a ten právě budeme testovat). Vstupem do funkce exchange je částka ve výchozí měně (cokoli mimo Kč) a kód výchozí měny („GBP“, „USD“ apod.). Výsledkem je hodnota v cílové měně (což jsou vždy Kč):

func exchange(amount float64, code string) float64 {
        rate := get_exchange_rate(code)
        return rate * amount
}

Pro získání kurzovního lístku můžeme použít následující funkci, která příslušný kurzovní lístek získá v čitelném a jednoduše parsovatelném textovém formátu (ostatně si to můžete vyzkoušet přímo v prohlížeči):

func get_exchange_rate(code string) float64 {
        const URL = "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt"
 
        response, err := http.Get(URL)
        if err != nil {
                panic("Connection refused")
        }
        defer response.Body.Close()
 
        fmt.Printf("Status: %s\n", response.Status)
        fmt.Printf("Content length: %d\n", response.ContentLength)
 
        scanner := bufio.NewScanner(response.Body)
        for scanner.Scan() {
                s := strings.Split(scanner.Text(), "|")
                if len(s) == 5 {
                        code_str := s[3]
                        rate_str := strings.Replace(s[4], ",", ".", 1)
                        if code == code_str {
                                rate_f, err := strconv.ParseFloat(rate_str, 64)
                                if err != nil {
                                        panic(err)
                                }
                                return rate_f
                        }
                }
        }
 
        return 0
}

Kde je tedy problém? Ten spočívá v tom, že budeme chtít otestovat funkci exchange, ovšem izolovaně od aktuálního kurzovního lístku. Navíc funkce pro stažení a parsing lístku nemusí pracovat vždy správně (například může být CI schován za firewallem, nebudeme chtít přetěžovat cizí webové služby atd.). Proto musíme funkci get_exchange_rate mockovat funkcí jednodušší.

17. Vytvoření mocku funkce get_exchange_rate_from_url

Celý příklad budeme muset nepatrně upravit. Nejprve nadeklarujeme nový datový typ pro jakýkoli „getter“ kurzovního lístku. Jedná se vždy o funkci akceptující kód měny a vracející hodnotu na lístku (například pro „USD“ by se vrátila hodnota 21.10 popř. podobná okamžitě platná hodnota):

type ExchangeDataGetter func(code string) float64

Jakákoli funkce tohoto typu (funkce je plnohodnotnou hodnotou!) bude uložena v jednoduché struktuře, jejíž prvek bude pojmenován právě get_exchange_rate:

type ExchangeGetter struct {
        get_exchange_rate ExchangeDataGetter
}

A nakonec nám zbývá vytvoření „konstruktoru“ vracejícího referenci na instanci předchozí struktury. To je nutné, protože ve skutečnosti budeme funkci volat jako metodu (viz další zdrojový text):

func NewExchangeGetter(g ExchangeDataGetter) *ExchangeGetter {
        return &ExchangeGetter{get_exchange_rate: g}
}

Samotná funkce exchange se změní na metodu, protože jejím příjemcem bude ukazatel na strukturu ExchangeGetter:

func (g *ExchangeGetter) exchange(amount float64, code string) float64 {
        rate := g.get_exchange_rate(code)
        return rate * amount
}
Poznámka: povšimněte si, jak se hodnota z kurzovního lístku získává – zápis odpovídá volání metody, ovšem ve skutečnosti „pouze“ získáme prvek ze struktury ExchangeGetter, což je zmíněná funkce získaná konstruktorem NewExchangeGetter:
g := NewExchangeGetter(get_exchange_rate_from_url)
fmt.Printf("%5.2f\n", g.exchange(10, "USD"))
 
g2 := NewExchangeGetter(get_exchange_rate_from_file)
fmt.Printf("%5.2f\n", g2.exchange(10, "USD"))
 
g3 := NewExchangeGetter(mocked_get_exchange_rate)
fmt.Printf("%5.2f\n", g2.exchange(10, "USD"))

Následně již můžeme nadeklarovat několik variant funkcí, které získají kód měny a vrátí hodnotu z kurzovního lístku (získaného buď z URL, ze souboru nebo se bude jednat o mock). Všechny tyto funkce mají stejný typ – ExchangeDataGetter:

root_podpora

func get_exchange_rate_from_url(code string) float64 {
        const URL = "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt"
 
        response, err := http.Get(URL)
        if err != nil {
                panic("Connection refused")
        }
        defer response.Body.Close()
 
        fmt.Printf("Status: %s\n", response.Status)
        fmt.Printf("Content length: %d\n", response.ContentLength)
 
        scanner := bufio.NewScanner(response.Body)
        for scanner.Scan() {
                s := strings.Split(scanner.Text(), "|")
                if len(s) == 5 {
                        code_str := s[3]
                        rate_str := strings.Replace(s[4], ",", ".", 1)
                        if code == code_str {
                                rate_f, err := strconv.ParseFloat(rate_str, 64)
                                if err != nil {
                                        panic(err)
                                }
                                return rate_f
                        }
                }
        }
 
        return 0
}
 
func get_exchange_rate_from_file(code string) float64 {
        const FILENAME = "kurzy.txt"
 
        file, err := os.Open(FILENAME)
        if err != nil {
                panic(err)
        }
        defer file.Close()
 
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
                s := strings.Split(scanner.Text(), "|")
                if len(s) == 5 {
                        code_str := s[3]
                        rate_str := strings.Replace(s[4], ",", ".", 1)
                        if code == code_str {
                                rate_f, err := strconv.ParseFloat(rate_str, 64)
                                if err != nil {
                                        panic(err)
                                }
                                return rate_f
                        }
                }
        }
 
        return 0
}
 
func mocked_get_exchange_rate(code string) float64 {
        return 21.5
}
Poznámka: povšimněte si, že se sice funkce jmenují odlišně, mají zcela jiné tělo, ovšem hlavička je shodná – stejné počty a typy parametrů i počty a typy návratových hodnot.

Úplný kód tedy může vypadat následovně:

package main
 
import (
        "bufio"
        "fmt"
        "net/http"
        "os"
        "strconv"
        "strings"
)
 
type ExchangeDataGetter func(code string) float64
 
type ExchangeGetter struct {
        get_exchange_rate ExchangeDataGetter
}
 
func NewExchangeGetter(g ExchangeDataGetter) *ExchangeGetter {
        return &ExchangeGetter{get_exchange_rate: g}
}
 
func get_exchange_rate_from_url(code string) float64 {
        const URL = "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt"
 
        response, err := http.Get(URL)
        if err != nil {
                panic("Connection refused")
        }
        defer response.Body.Close()
 
        fmt.Printf("Status: %s\n", response.Status)
        fmt.Printf("Content length: %d\n", response.ContentLength)
 
        scanner := bufio.NewScanner(response.Body)
        for scanner.Scan() {
                s := strings.Split(scanner.Text(), "|")
                if len(s) == 5 {
                        code_str := s[3]
                        rate_str := strings.Replace(s[4], ",", ".", 1)
                        if code == code_str {
                                rate_f, err := strconv.ParseFloat(rate_str, 64)
                                if err != nil {
                                        panic(err)
                                }
                                return rate_f
                        }
                }
        }
 
        return 0
}
 
func get_exchange_rate_from_file(code string) float64 {
        const FILENAME = "kurzy.txt"
 
        file, err := os.Open(FILENAME)
        if err != nil {
                panic(err)
        }
        defer file.Close()
 
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
                s := strings.Split(scanner.Text(), "|")
                if len(s) == 5 {
                        code_str := s[3]
                        rate_str := strings.Replace(s[4], ",", ".", 1)
                        if code == code_str {
                                rate_f, err := strconv.ParseFloat(rate_str, 64)
                                if err != nil {
                                        panic(err)
                                }
                                return rate_f
                        }
                }
        }
 
        return 0
}
 
func mocked_get_exchange_rate(code string) float64 {
        return 21.5
}
 
func (g *ExchangeGetter) exchange(amount float64, code string) float64 {
        rate := g.get_exchange_rate(code)
        return rate * amount
}
 
func main() {
        g := NewExchangeGetter(get_exchange_rate_from_file)
        fmt.Printf("%5.2f\n", g.exchange(10, "USD"))
        g2 := NewExchangeGetter(mocked_get_exchange_rate)
        fmt.Printf("%5.2f\n", g2.exchange(10, "USD"))
}

18. Repositář s demonstračními příklady

Zdrojové kódy všech dnes popsaných demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně jeden megabajt), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

# Soubor Popis Cesta
1 add.go modul obsahující testovanou funkci Add https://github.com/tisnik/go-root/blob/master/article17/tes­ts01/add.go
1 add_test.go jednotkové testy funkce Add https://github.com/tisnik/go-root/blob/master/article17/tes­ts01/add_test.go
     
2 add.go modul obsahující testovanou funkci add https://github.com/tisnik/go-root/blob/master/article17/tes­ts02/add.go
2 add_test.go jednotkové testy funkce add https://github.com/tisnik/go-root/blob/master/article17/tes­ts02/add_test.go
     
3 add.go funkce add upravená takovým způsobem, aby vracela špatné výsledky https://github.com/tisnik/go-root/blob/master/article17/tes­ts03/add.go
3 add_test.go rozšířené testy pro funkci add https://github.com/tisnik/go-root/blob/master/article17/tes­ts03/add_test.go
     
4 add.go funkce add upravená takovým způsobem, aby vracela špatné výsledky https://github.com/tisnik/go-root/blob/master/article17/tes­ts04/add.go
4 add_test.go testy používající metody Log+FailNow namísto Error https://github.com/tisnik/go-root/blob/master/article17/tes­ts04/add_test.go
     
5 add.go opět funkce add, tentokrát se správnými výsledky https://github.com/tisnik/go-root/blob/master/article17/tes­ts05/add.go
5 add_test.go jednotkový test řízený tabulkou https://github.com/tisnik/go-root/blob/master/article17/tes­ts05/add_test.go
     
6 add.go opět funkce add, tentokrát se špatnými výsledky https://github.com/tisnik/go-root/blob/master/article17/tes­ts06/add.go
6 add_test.go jednotkový test řízený tabulkou https://github.com/tisnik/go-root/blob/master/article17/tes­ts06/add_test.go
     
7 add.go implementace funkce add https://github.com/tisnik/go-root/blob/master/article17/tes­ts07/add.go
7 add_test.go testy pojmenované takovým způsobem, aby byly snadno vybíratelné https://github.com/tisnik/go-root/blob/master/article17/tes­ts07/add_test.go
     
8 add.go implementace funkce add https://github.com/tisnik/go-root/blob/master/article17/tes­ts08/add.go
8 add_test.go standardní sada jednotkových testů https://github.com/tisnik/go-root/blob/master/article17/tes­ts08/add_test.go
8 add_fast_test.go modul používající strukturovaný komentář +build fast https://github.com/tisnik/go-root/blob/master/article17/tes­ts08/add_fast_test.go
8 add_slow_test.go modul používající strukturovaný komentář +build slow https://github.com/tisnik/go-root/blob/master/article17/tes­ts08/add_slow_test.go
     
9 factorial.go funkce pro výpočet faktoriálu https://github.com/tisnik/go-root/blob/master/article17/fac­torial/factorial.go
9 factorial_test.go jednotkové testy funkce pro výpočet faktoriálu https://github.com/tisnik/go-root/blob/master/article17/fac­torial/factorial_test.go
     
10 factorial.go výpočet faktoriálu podruhé https://github.com/tisnik/go-root/blob/master/article17/fac­torial_convey/factorial.go
10 behaviour_test.go BDD testy funkce pro výpočet faktoriálu https://github.com/tisnik/go-root/blob/master/article17/fac­torial_convey/behaviour_tes­t.go
     
11 kurzy.go převod měny na základě kurzovního lístku https://github.com/tisnik/go-root/blob/master/article17/moc­king/kurzy.go
11 kurzy.txt kurzovní lístek https://github.com/tisnik/go-root/blob/master/article17/moc­king/kurzy.txt

19. Odkazy na Internetu

  1. Package testing
    https://golang.org/pkg/testing/
  2. Golang basics – writing unit tests
    https://blog.alexellis.io/golang-writing-unit-tests/
  3. An Introduction to Programming in Go / Testing
    https://www.golang-book.com/books/intro/12
  4. An Introduction to Testing in Go
    https://tutorialedge.net/golang/intro-testing-in-go/
  5. Advanced Go Testing Tutorial
    https://tutorialedge.net/go­lang/advanced-go-testing-tutorial/
  6. GoConvey
    http://goconvey.co/
  7. Testing Techniques
    https://talks.golang.org/2014/tes­ting.slide
  8. 5 simple tips and tricks for writing unit tests in #golang
    https://medium.com/@matryer/5-simple-tips-and-tricks-for-writing-unit-tests-in-golang-619653f90742
  9. Afinní transformace
    https://cs.wikibooks.org/wi­ki/Geometrie/Afinn%C3%AD_tran­sformace_sou%C5%99adnic
  10. package gg
    https://godoc.org/github.com/fo­gleman/gg
  11. Generate an animated GIF with Golang
    http://tech.nitoyon.com/en/blog/2016/01/07/­go-animated-gif-gen/
  12. Generate an image programmatically with Golang
    http://tech.nitoyon.com/en/blog/2015/12/31/­go-image-gen/
  13. The Go image package
    https://blog.golang.org/go-image-package
  14. Balíček draw2D: 2D rendering for different output (raster, pdf, svg)
    https://github.com/llgcode/draw2d
  15. Draw a rectangle in Golang?
    https://stackoverflow.com/qu­estions/28992396/draw-a-rectangle-in-golang
  16. YAML
    https://yaml.org/
  17. edn
    https://github.com/edn-format/edn
  18. Smile
    https://github.com/FasterXML/smile-format-specification
  19. Protocol-Buffers
    https://developers.google.com/protocol-buffers/
  20. Marshalling (computer science)
    https://en.wikipedia.org/wi­ki/Marshalling_(computer_sci­ence)
  21. Unmarshalling
    https://en.wikipedia.org/wi­ki/Unmarshalling
  22. Introducing JSON
    http://json.org/
  23. Package json
    https://golang.org/pkg/encoding/json/
  24. The Go Blog: JSON and Go
    https://blog.golang.org/json-and-go
  25. Go by Example: JSON
    https://gobyexample.com/json
  26. Writing Web Applications
    https://golang.org/doc/articles/wiki/
  27. Golang Web Apps
    https://www.reinbach.com/blog/golang-webapps-1/
  28. Build web application with Golang
    https://legacy.gitbook.com/bo­ok/astaxie/build-web-application-with-golang/details
  29. Golang Templates – Golang Web Pages
    https://www.youtube.com/wat­ch?v=TkNIETmF-RU
  30. Simple Golang HTTPS/TLS Examples
    https://github.com/denji/golang-tls
  31. Playing with images in HTTP response in golang
    https://www.sanarias.com/blog/1214Pla­yingwithimagesinHTTPrespon­seingolang
  32. MIME Types List
    https://www.freeformatter.com/mime-types-list.html
  33. Go Mutex Tutorial
    https://tutorialedge.net/golang/go-mutex-tutorial/
  34. Creating A Simple Web Server With Golang
    https://tutorialedge.net/go­lang/creating-simple-web-server-with-golang/
  35. Building a Web Server in Go
    https://thenewstack.io/building-a-web-server-in-go/
  36. How big is the pipe buffer?
    https://unix.stackexchange­.com/questions/11946/how-big-is-the-pipe-buffer
  37. How to turn off buffering of stdout in C
    https://stackoverflow.com/qu­estions/7876660/how-to-turn-off-buffering-of-stdout-in-c
  38. setbuf(3) – Linux man page
    https://linux.die.net/man/3/setbuf
  39. setvbuf(3) – Linux man page (stejný obsah jako předchozí stránka)
    https://linux.die.net/man/3/setvbuf
  40. Select waits on a group of channels
    https://yourbasic.org/golang/select-explained/
  41. Rob Pike: Simplicity is Complicated (video)
    http://www.golang.to/posts/dotgo-2015-rob-pike-simplicity-is-complicated-youtube-16893
  42. Algorithms to Go
    https://yourbasic.org/
  43. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů
    https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu/
  44. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů: vlastní filtry a lexery
    https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu-vlastni-filtry-a-lexery/
  45. Go Defer Simplified with Practical Visuals
    https://blog.learngoprogram­ming.com/golang-defer-simplified-77d3b2b817ff
  46. 5 More Gotchas of Defer in Go — Part II
    https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa
  47. The Go Blog: Defer, Panic, and Recover
    https://blog.golang.org/defer-panic-and-recover
  48. The defer keyword in Swift 2: try/finally done right
    https://www.hackingwithswift.com/new-syntax-swift-2-defer
  49. Swift Defer Statement
    https://andybargh.com/swift-defer-statement/
  50. Modulo operation (Wikipedia)
    https://en.wikipedia.org/wi­ki/Modulo_operation
  51. Node.js vs Golang: Battle of the Next-Gen Languages
    https://www.hostingadvice­.com/blog/nodejs-vs-golang/
  52. The Go Programming Language (home page)
    https://golang.org/
  53. GoDoc
    https://godoc.org/
  54. Go (programming language), Wikipedia
    https://en.wikipedia.org/wi­ki/Go_(programming_langua­ge)
  55. Go Books (kniha o jazyku Go)
    https://github.com/dariubs/GoBooks
  56. The Go Programming Language Specification
    https://golang.org/ref/spec
  57. Go: the Good, the Bad and the Ugly
    https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/
  58. Package builtin
    https://golang.org/pkg/builtin/
  59. Package fmt
    https://golang.org/pkg/fmt/
  60. The Little Go Book (další kniha)
    https://github.com/dariubs/GoBooks
  61. The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
    https://www.safaribookson­line.com/library/view/the-go-programming/9780134190570/e­book_split010.html
  62. Learning Go
    https://www.miek.nl/go/
  63. Go Bootcamp
    http://www.golangbootcamp.com/
  64. Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
    http://www.informit.com/sto­re/programming-in-go-creating-applications-for-the-21st-9780321774637
  65. Introducing Go (Build Reliable, Scalable Programs)
    http://shop.oreilly.com/pro­duct/0636920046516.do
  66. Learning Go Programming
    https://www.packtpub.com/application-development/learning-go-programming
  67. The Go Blog
    https://blog.golang.org/
  68. Getting to Go: The Journey of Go's Garbage Collector
    https://blog.golang.org/ismmkeynote
  69. Go (programovací jazyk, Wikipedia)
    https://cs.wikipedia.org/wi­ki/Go_(programovac%C3%AD_ja­zyk)
  70. Rychle, rychleji až úplně nejrychleji s jazykem Go
    https://www.root.cz/clanky/rychle-rychleji-az-uplne-nejrychleji-s-jazykem-go/
  71. Installing Go on the Raspberry Pi
    https://dave.cheney.net/2012/09/25/in­stalling-go-on-the-raspberry-pi
  72. How the Go runtime implements maps efficiently (without generics)
    https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics
  73. Niečo málo o Go – Golang (slovensky)
    http://golangsk.logdown.com/
  74. How Many Go Developers Are There?
    https://research.swtch.com/gop­hercount
  75. Most Popular Technologies (Stack Overflow Survery 2018)
    https://insights.stackover­flow.com/survey/2018/#most-popular-technologies
  76. Most Popular Technologies (Stack Overflow Survery 2017)
    https://insights.stackover­flow.com/survey/2017#techno­logy
  77. JavaScript vs. Golang for IoT: Is Gopher Winning?
    https://www.iotforall.com/javascript-vs-golang-iot/
  78. The Go Programming Language: Release History
    https://golang.org/doc/de­vel/release.html
  79. Go 1.11 Release Notes
    https://golang.org/doc/go1.11
  80. Go 1.10 Release Notes
    https://golang.org/doc/go1.10
  81. Go 1.9 Release Notes (tato verze je stále používána)
    https://golang.org/doc/go1.9
  82. Go 1.8 Release Notes (i tato verze je stále používána)
    https://golang.org/doc/go1.8
  83. Go on Fedora
    https://developer.fedorapro­ject.org/tech/languages/go/go-installation.html
  84. Writing Go programs
    https://developer.fedorapro­ject.org/tech/languages/go/go-programs.html
  85. The GOPATH environment variable
    https://tip.golang.org/doc/co­de.html#GOPATH
  86. Command gofmt
    https://tip.golang.org/cmd/gofmt/
  87. The Go Blog: go fmt your code
    https://blog.golang.org/go-fmt-your-code
  88. C? Go? Cgo!
    https://blog.golang.org/c-go-cgo
  89. Spaces vs. Tabs: A 20-Year Debate Reignited by Google’s Golang
    https://thenewstack.io/spaces-vs-tabs-a-20-year-debate-and-now-this-what-the-hell-is-wrong-with-go/
  90. 400,000 GitHub repositories, 1 billion files, 14 terabytes of code: Spaces or Tabs?
    https://medium.com/@hoffa/400–000-github-repositories-1-billion-files-14-terabytes-of-code-spaces-or-tabs-7cfe0b5dd7fd
  91. Gofmt No Longer Allows Spaces. Tabs Only
    https://news.ycombinator.com/i­tem?id=7914523
  92. Why does Go „go fmt“ uses tabs instead of whitespaces?
    https://www.quora.com/Why-does-Go-go-fmt-uses-tabs-instead-of-whitespaces
  93. Interactive: The Top Programming Languages 2018
    https://spectrum.ieee.org/sta­tic/interactive-the-top-programming-languages-2018
  94. Go vs. Python
    https://www.peterbe.com/plog/govspy
  95. PackageManagementTools
    https://github.com/golang/go/wi­ki/PackageManagementTools
  96. A Tour of Go: Type inference
    https://tour.golang.org/basics/14
  97. Go Slices: usage and internals
    https://blog.golang.org/go-slices-usage-and-internals
  98. Go by Example: Slices
    https://gobyexample.com/slices
  99. What is the point of slice type in Go?
    https://stackoverflow.com/qu­estions/2098874/what-is-the-point-of-slice-type-in-go
  100. The curious case of Golang array and slices
    https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335
  101. Introduction to Slices in Golang
    https://www.callicoder.com/golang-slices/
  102. Golang: Understanding ‚null‘ and nil
    https://newfivefour.com/golang-null-nil.html
  103. What does nil mean in golang?
    https://stackoverflow.com/qu­estions/35983118/what-does-nil-mean-in-golang
  104. nils In Go
    https://go101.org/article/nil.html
  105. Go slices are not dynamic arrays
    https://appliedgo.net/slices/
  106. Go-is-no-good (nelze brát doslova)
    https://github.com/ksimka/go-is-not-good
  107. Rust vs. Go
    https://news.ycombinator.com/i­tem?id=13430108
  108. Seriál Programovací jazyk Rust
    https://www.root.cz/seria­ly/programovaci-jazyk-rust/
  109. Modern garbage collection: A look at the Go GC strategy
    https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e
  110. Go GC: Prioritizing low latency and simplicity
    https://blog.golang.org/go15gc
  111. Is Golang a good language for embedded systems?
    https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems
  112. Running GoLang on an STM32 MCU. A quick tutorial.
    https://www.mickmake.com/post/running-golang-on-an-mcu-a-quick-tutorial
  113. Go, Robot, Go! Golang Powered Robotics
    https://gobot.io/
  114. Emgo: Bare metal Go (language for programming embedded systems)
    https://github.com/ziutek/emgo
  115. UTF-8 history
    https://www.cl.cam.ac.uk/~mgk25/uc­s/utf-8-history.txt
  116. Less is exponentially more
    https://commandcenter.blog­spot.com/2012/06/less-is-exponentially-more.html
  117. Should I Rust, or Should I Go
    https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9
  118. Setting up and using gccgo
    https://golang.org/doc/install/gccgo
  119. Elastic Tabstops
    http://nickgravgaard.com/elastic-tabstops/
  120. Strings, bytes, runes and characters in Go
    https://blog.golang.org/strings
  121. Datový typ
    https://cs.wikipedia.org/wi­ki/Datov%C3%BD_typ
  122. Seriál o programovacím jazyku Rust: Základní (primitivní) datové typy
    https://www.root.cz/clanky/pro­gramovaci-jazyk-rust-nahrada-c-nebo-slepa-cesta/#k09
  123. Seriál o programovacím jazyku Rust: Vytvoření „řezu“ z pole
    https://www.root.cz/clanky/prace-s-poli-v-programovacim-jazyku-rust/#k06
  124. Seriál o programovacím jazyku Rust: Řezy (slice) vektoru
    https://www.root.cz/clanky/prace-s-vektory-v-programovacim-jazyku-rust/#k05
  125. Printf Format Strings
    https://www.cprogramming.com/tu­torial/printf-format-strings.html
  126. Java: String.format
    https://docs.oracle.com/ja­vase/8/docs/api/java/lang/Strin­g.html#format-java.lang.String-java.lang.Object…-
  127. Java: format string syntax
    https://docs.oracle.com/ja­vase/8/docs/api/java/util/For­matter.html#syntax
  128. Selectors
    https://golang.org/ref/spec#Selectors
  129. Calling Go code from Python code
    http://savorywatt.com/2015/09/18/ca­lling-go-code-from-python-code/
  130. Go Data Structures: Interfaces
    https://research.swtch.com/interfaces
  131. How to use interfaces in Go
    http://jordanorelli.com/pos­t/32665860244/how-to-use-interfaces-in-go
  132. Interfaces in Go (part I)
    https://medium.com/golangspec/in­terfaces-in-go-part-i-4ae53a97479c
  133. Part 21: Goroutines
    https://golangbot.com/goroutines/
  134. Part 22: Channels
    https://golangbot.com/channels/
  135. [Go] Lightweight eventbus with async compatibility for Go
    https://github.com/asaske­vich/EventBus
  136. What about Trait support in Golang?
    https://www.reddit.com/r/go­lang/comments/8mfykl/what_a­bout_trait_support_in_golan­g/
  137. Don't Get Bitten by Pointer vs Non-Pointer Method Receivers in Golang
    https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/
  138. Control Flow
    https://en.wikipedia.org/wi­ki/Control_flow
  139. Structured programming
    https://en.wikipedia.org/wi­ki/Structured_programming
  140. Control Structures
    https://www.golang-book.com/books/intro/5
  141. Control structures – Go if else statement
    http://golangtutorials.blog­spot.com/2011/06/control-structures-if-else-statement.html
  142. Control structures – Go switch case statement
    http://golangtutorials.blog­spot.com/2011/06/control-structures-go-switch-case.html
  143. Control structures – Go for loop, break, continue, range
    http://golangtutorials.blog­spot.com/2011/06/control-structures-go-for-loop-break.html
  144. Single Function Exit Point
    http://wiki.c2.com/?Single­FunctionExitPoint
  145. Entry point
    https://en.wikipedia.org/wi­ki/Entry_point
  146. Why does Go have a GOTO statement?!
    https://www.reddit.com/r/go­lang/comments/kag5q/why_do­es_go_have_a_goto_statemen­t/
  147. Effective Go
    https://golang.org/doc/ef­fective_go.html
  148. GoClipse: an Eclipse IDE for the Go programming language
    http://goclipse.github.io/
  149. GoClipse Installation
    https://github.com/GoClip­se/goclipse/blob/latest/do­cumentation/Installation.md#in­stallation

Byl pro vás článek přínosný?