Obsah
1. Jazyk Go prakticky: jednotkové testy kódu, který přistupuje k SQL databázím
2. Přístup k relačním (SQL databázím z jazyka Go
3. Databázové ovladače použitelné v jazyku Go
4. Příprava databáze používané testovanou aplikací
5. Vytvoření a naplnění tabulky používané testovanou aplikací
6. První verze testované aplikace
7. Popis jednotlivých částí testované aplikace
8. Spuštění testované aplikace
9. Jednotkové testy v programovacím jazyce Go
10. Příklad vytvoření jednotkových testů
11. Mockování funkcí a metod pro potřeby jednotkových testů
12. Mockování funkcí a metod přistupujících do relační databáze s využitím knihovny go-sqlmock
13. Export testovaných funkcí a kostra jednotkových testů
14. Základní otestování funkce DisplayAllRecords
15. Přidání řádků, které SQL mock vrátí testované funkci jako výsledek SQL dotazu
16. Test funkce vracející data načtená z databáze
17. Funkce se složitějším SQL dotazem s klauzulí WHERE
19. Repositář s demonstračními příklady
1. Jazyk Go prakticky: jednotkové testy kódu, který přistupuje k SQL databázím
Již v úvodních částech seriálu o programovacím jazyce Go jsme si řekli, že se tento programovací jazyk velmi často – a nutno říci, že i s úspěchem – používá pro implementaci různých webových služeb a mikroslužeb. Této problematice jsme se ostatně věnovali i v tomto článku. Ovšem mnohé služby pochopitelně potřebují používat nějakou formu databáze pro zajištění persistence dat. Může se jednat o relační databáze, dokumentové databáze, grafové databáze, objektové databáze atd. Způsob práce s relačními databázemi jsme si již taktéž popsali, a to včetně zmínky o několika systémech pro ORM. Popsali jsme si taktéž propojení aplikací psaných v jazyce Go s nerelační databází Redis.
Dnes se ovšem zaměříme na poněkud odlišné téma – jak vlastně testovat aplikace, které databázi používají. Pro integrační a end-to-end testy lze samozřejmě použít reálnou databázi (pochopitelně odlišnou instanci, než je instance produkční), ovšem v případě jednotkových testů je situace nepatrně složitější, neboť v nich budeme většinou potřebovat nahradit reálnou databázi nějakou formou mocku – buď databází ležící v operační paměti, nebo mohou být mockovány přímo databázové operace.
2. Přístup k relačním (SQL) databázím z jazyka Go
Pro přístup k relačním databázím (resp. přesněji řečeno k databázím používajícím jazyk SQL jako svůj doménově specifický jazyk) slouží v programovacím jazyce Go standardní balíček nazvaný database/sql. Tento balíček zajišťuje navázání připojení k databázi, spouštění příkazů na straně databáze (jak DDL – Data Definition Language, tak i DML a DQL, tedy zjednodušeně řečeno příkazy SELECT, INSERT, UPDATE a DELETE), umožňuje zpracování výsledků příkazu SELECT, parsing záznamů, datové převody (časová razítka atd.), zajišťuje předávání parametrů všem DML i DQL příkazům atd. Všechny tyto podporované operace jsou přitom obecně nezávislé na použité databázi, protože balíček database/sql používá pro přístup ke konkrétní databázi takzvané databázové ovladače (drivers).
V praxi to znamená zejména to, že aplikace může být napsána obecně, přičemž konkrétní databázový ovladač bude vybrán až na základě aktuální konfigurace. Příkladem může být využití databáze PostgreSQL při lokálním nasazení, popř. ve vývojovém prostředí, zatímco na předprodukčním a produkčním prostředí bude použita AWS RDS. Podobně – pokud budou SQL příkazy napsány dostatečně přenositelně – lze v testech použít databázi SQLite běžící pouze v operační paměti.
3. Databázové ovladače použitelné v jazyku Go
V současnosti existuje několik desítek ovladačů umožňujících propojení aplikací psaných v jazyku Go s databázemi. Nejdůležitější ovladače jsou zmíněny v následující tabulce, ovšem s tím, že počet použitelných ovladačů i počet podporovaných databází neustále roste:
4. Příprava databáze používané testovanou aplikací
Aplikace (resp. přesněji řečeno kostra aplikace), pro kterou budeme v rámci dalších kapitol vytvářet jednotkové testy, přistupuje k databázi typu PostgreSQL. Jméno databáze bude testdb, přičemž tato databáze bude obsahovat jedinou tabulku persons se třemi sloupci. V této kapitole si ukážeme, jak takovou databázi připravit, v navazující kapitole, jak vytvořit a naplnit tabulku persons.
Nejprve je nutné PostgreSQL nainstalovat (apt-get, dnf, yum atd.) a spustit. Použít se může „univerzální“ příkaz:
# service postgresql start
nebo přímo příkaz pro systemd:
# systemctl start postgresql
Přesvědčíme se, že databáze skutečně běží:
$ systemctl status postgresql ● postgresql.service - PostgreSQL database server Loaded: loaded (/usr/lib/systemd/system/postgresql.service; disabled; vendor Active: active (running) since Mon 2021-03-01 12:10:01 CET; 2 weeks 1 days ag Process: 27550 ExecStart=/usr/libexec/postgresql-ctl start -D ${PGDATA} -s -w Process: 27548 ExecStartPre=/usr/libexec/postgresql-check-db-dir postgresql (c Main PID: 27553 (postgres) Tasks: 7 (limit: 4915) CGroup: /system.slice/postgresql.service ├─27553 /usr/bin/postgres -D /var/lib/pgsql/data ├─27554 postgres: logger process ├─27556 postgres: checkpointer process ├─27557 postgres: writer process ├─27558 postgres: wal writer process ├─27559 postgres: autovacuum launcher process └─27560 postgres: stats collector process
Dále se připojíme k databázi řádkovým klientem psql:
$ psql -U postgres Password for user postgres: (zde se taktéž použije nastavené heslo) psql (9.6.10) Type "help" for help. postgres=#
Po připojení vytvoříme novou databázi pojmenovanou testdb:
postgres=# CREATE DATABASE testdb; CREATE DATABASE
Pro jistotu se podíváme, jestli byla databáze skutečně vytvořena:
postgres=# \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges ------------+----------+----------+-------------+-------------+----------------------- aggregator | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | controller | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | ptisnovs | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres test | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | testdb | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | (8 rows)
5. Vytvoření a naplnění tabulky používané testovanou aplikací
Nyní nám zbývá vytvořit a naplnit tabulku persons, která bude následně využita testovanou aplikací. Přepneme se tedy (stále v interaktivním prostředí psql) do databáze testdb:
postgres=# \c testdb You are now connected to database "testdb" as user "postgres".
Dále vytvoříme tabulku používanou aplikací (tedy využijeme DDL, které je součástí SQL):
CREATE TABLE persons( id SERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, surname VARCHAR(50) NOT NULL);
Pro jistotu zkontrolujeme, zda tabulka existuje a jakou má strukturu:
testdb=# \dt public | persons | table | postgres
testdb=# \d persons id | integer | not null default nextval('persons_id_seq'::regclass) name | character varying(50) | not null surname | character varying(50) | not null
Předposledním krokem je naplnění této tabulky sedmi záznamy. Jedná se o postavy ze známé Cimrmanovské hry Švestka:
INSERT INTO persons(name, surname) VALUES('Eliška', 'Najbrtová'); INSERT INTO persons(name, surname) VALUES('Jenny', 'Suk'); INSERT INTO persons(name, surname) VALUES('Anička', 'Šafářová'); INSERT INTO persons(name, surname) VALUES('Sváťa', 'Pulec'); INSERT INTO persons(name, surname) VALUES('Blažej', 'Motyčka'); INSERT INTO persons(name, surname) VALUES('Eda', 'Wasserfall'); INSERT INTO persons(name, surname) VALUES('Přemysl', 'Hájek');
V posledním kroku pouze zkontrolujeme, zda tabulka persons skutečně obsahuje všech sedm postav:
testdb=# select * from persons; 1 | Eliška | Najbrtová 2 | Jenny | Suk 3 | Anička | Šafářová 4 | Sváťa | Pulec 5 | Blažej | Motyčka 6 | Eda | Wasserfall 7 | Přemysl | Hájek
Klienta psql nyní již můžeme opustit:
postgres=# \q
6. První verze testované aplikace
V dalším kroku si připravíme první verzi aplikace, která bude k databázi přistupovat a pro kterou postupně vytvoříme jednotkové testy. Nejprve vytvoříme kostru nového projektu psaného v jazyce Go, a to konkrétně příkazem:
$ go mod init db-test
Měl by se vytvořit soubor go.mod, jehož původní verze bude vypadat následovně:
module db-test go 1.14
První verze testované aplikace vypadá následovně:
package main import ( "fmt" "os" "database/sql" _ "github.com/lib/pq" // PostgreSQL database driver _ "github.com/mattn/go-sqlite3" // SQLite database driver "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) // Datová struktura s konfigurací připojení k databázi type StorageConfiguration struct { Driver string `mapstructure:"db_driver" toml:"db_driver"` SQLiteDataSource string `mapstructure:"sqlite_datasource" toml:"sqlite_datasource"` PGUsername string `mapstructure:"pg_username" toml:"pg_username"` PGPassword string `mapstructure:"pg_password" toml:"pg_password"` PGHost string `mapstructure:"pg_host" toml:"pg_host"` PGPort int `mapstructure:"pg_port" toml:"pg_port"` PGDBName string `mapstructure:"pg_db_name" toml:"pg_db_name"` PGParams string `mapstructure:"pg_params" toml:"pg_params"` } // Chybové zprávy const ( canNotConnectToDataStorageMessage = "Can not connect to data storage" connectionToDBNotEstablished = "Connection to database not established" unableToCloseDBRowsHandle = "Unable to close the DB rows handle" databaseOperationFailed = "Database operation failed" ) // Inicializace připojení k databázi func initDatabaseConnection(configuration StorageConfiguration) (*sql.DB, error) { driverName := configuration.Driver dataSource := "" log.Info().Str("driverName", configuration.Driver).Msg("DB connection configuration") // inicializace připojení s vybraným driverem switch driverName { case "sqlite3": //driverType := DBDriverSQLite3 //driver = &sqlite3.SQLiteDriver{} dataSource = configuration.SQLiteDataSource case "postgres": //driverType := DBDriverPostgres //driver = &pq.Driver{} dataSource = fmt.Sprintf( "postgresql://%v:%v@%v:%v/%v?%v", configuration.PGUsername, configuration.PGPassword, configuration.PGHost, configuration.PGPort, configuration.PGDBName, configuration.PGParams, ) default: // neznámý driver err := fmt.Errorf("driver %v is not supported", driverName) log.Err(err).Msg(canNotConnectToDataStorageMessage) return nil, err } // pokus o inicializaci připojení k databázi connection, err := sql.Open(driverName, dataSource) // test, zda bylo připojení k databázi úspěšné if err != nil { log.Err(err).Msg(canNotConnectToDataStorageMessage) return nil, err } return connection, nil } // Zobrazení všech záznamů v tabulce "persons" func displayAllRecords(connection *sql.DB) error { // dotaz do databáze query := "SELECT id, name, surname FROM persons" rows, err := connection.Query(query) // test, zda byl SQL příkaz proveden bez chyby if err != nil { return err } defer func() { // pokud dojde k chybě nebo na konci smyčky, musíme uvolnit prostředky if closeErr := rows.Close(); closeErr != nil { log.Error().Err(closeErr).Msg(unableToCloseDBRowsHandle) } }() // projít všemi vrácenými řádky for rows.Next() { var ( id int name string surname string ) // přečtení dat z jednoho vráceného řádku if err := rows.Scan(&id, &name, &surname); err != nil { return err } // výpis načteného záznamu log.Info().Int("ID", id). Str("name", name). Str("surname", surname). Msg("Record") } return nil } // Vložení nového záznamu do tabulky "persons" func insertRecord(connection *sql.DB, name string, surname string) (int, error) { // provedení SQL příkazu se dvěma parametry sqlStatement := "INSERT INTO persons (name, surname) VALUES($1, $2);" result, err := connection.Exec(sqlStatement, name, surname) // test, zda byl SQL příkaz proveden bez chyby if err != nil { return 0, err } // přečíst počet řádků v tabulce, které byly SQL příkazem upraveny affected, err := result.RowsAffected() // i tato operace může teoreticky skončit s chybou nebo nemusí být podporována if err != nil { return 0, err } return int(affected), nil } // Vymazání záznamu nebo záznamů na základě zapsaného jména func deleteByName(connection *sql.DB, name string) (int, error) { // provedení SQL příkazu s jedním parametrem sqlStatement := "DELETE FROM persons WHERE name = $1;" result, err := connection.Exec(sqlStatement, name) // test, zda byl SQL příkaz proveden bez chyby if err != nil { return 0, err } // přečíst počet řádků v tabulce, které byly SQL příkazem upraveny affected, err := result.RowsAffected() // i tato operace může teoreticky skončit s chybou nebo nemusí být podporována if err != nil { return 0, err } return int(affected), nil } func main() { // nastavit logovací systém pro barevný výstup na terminál log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) // konfigurace připojení do databáze config := StorageConfiguration{ Driver: "postgres", PGUsername: "postgres", PGPassword: "postgres", PGHost: "localhost", PGPort: 5432, PGDBName: "testdb", PGParams: "sslmode=disable", } log.Debug().Msg("Started") // inicializace připojení k databázi connection, err := initDatabaseConnection(config) if err != nil { log.Err(err).Msg(connectionToDBNotEstablished) return } // přečtení všech záznamů z tabulky "persons" err = displayAllRecords(connection) if err != nil { log.Err(err).Msg(databaseOperationFailed) return } // vymazání záznamu či záznamů na základě zapsaného jména affected, err := deleteByName(connection, "Eda") if err != nil { log.Err(err).Msg(databaseOperationFailed) return } log.Info().Int("deleted rows", affected).Msg("DELETE") // přidání nového záznamu do databáze affected, err = insertRecord(connection, "Eda", "Vodopád") if err != nil { log.Err(err).Msg(databaseOperationFailed) return } log.Info().Int("inserted rows", affected).Msg("INSERT") // přečtení všech záznamů z tabulky "persons" err = displayAllRecords(connection) if err != nil { log.Err(err).Msg(databaseOperationFailed) return } log.Debug().Msg("Finished") }
7. Popis jednotlivých částí testované aplikace
Pojďme si nyní alespoň ve stručnosti popsat jednotlivé části testované aplikace.
Po uvedení jména balíčku následuje import dalších balíčků. Povšimněte si, že se importují dva balíčky obsahující drivery podporovaných SQL databází. Funkce ani struktury z těchto driverů nebudeme používat, takže se namísto skutečných jmen jako jmenný alias použije podtržítko (jinak by se jednalo o chybu zjišťovanou při překladu), ovšem import být proveden musí, neboť je používán linkerem. Další dva importované balíčky jsou použity pro logování prováděných operací na standardním výstupu (v ekosystému jazyka Go se jedná o poměrně populární balíčky):
package main import ( "fmt" "os" "database/sql" _ "github.com/lib/pq" // PostgreSQL database driver _ "github.com/mattn/go-sqlite3" // SQLite database driver "github.com/rs/zerolog" "github.com/rs/zerolog/log" )
Následuje deklarace datové struktury použité pro specifikaci připojení ke konkrétní databázi. Tato struktura je nastavena takovým způsobem, že ji lze načíst ze souborů typu TOML atd.:
// Datová struktura s konfigurací připojení k databázi type StorageConfiguration struct { Driver string `mapstructure:"db_driver" toml:"db_driver"` SQLiteDataSource string `mapstructure:"sqlite_datasource" toml:"sqlite_datasource"` PGUsername string `mapstructure:"pg_username" toml:"pg_username"` PGPassword string `mapstructure:"pg_password" toml:"pg_password"` PGHost string `mapstructure:"pg_host" toml:"pg_host"` PGPort int `mapstructure:"pg_port" toml:"pg_port"` PGDBName string `mapstructure:"pg_db_name" toml:"pg_db_name"` PGParams string `mapstructure:"pg_params" toml:"pg_params"` }
Další část zdrojového kódu pouze obsahuje konstanty s chybovými zprávami. Některé z těchto zpráv jsou totiž použity na více místech:
// Chybové zprávy const ( canNotConnectToDataStorageMessage = "Can not connect to data storage" connectionToDBNotEstablished = "Connection to database not established" unableToCloseDBRowsHandle = "Unable to close the DB rows handle" databaseOperationFailed = "Database operation failed" )
Mnohem zajímavější je funkce nazvaná initDatabaseConnection. Ta totiž podle zvoleného databázového driveru zajistí připojení k databázi. Řetězec použitý pro specifikaci připojení (connection string) je pro každou databázi odlišný:
// Inicializace připojení k databázi func initDatabaseConnection(configuration StorageConfiguration) (*sql.DB, error) { driverName := configuration.Driver dataSource := "" log.Info().Str("driverName", configuration.Driver).Msg("DB connection configuration") // inicializace připojení s vybraným driverem switch driverName { case "sqlite3": //driverType := DBDriverSQLite3 //driver = &sqlite3.SQLiteDriver{} dataSource = configuration.SQLiteDataSource case "postgres": //driverType := DBDriverPostgres //driver = &pq.Driver{} dataSource = fmt.Sprintf( "postgresql://%v:%v@%v:%v/%v?%v", configuration.PGUsername, configuration.PGPassword, configuration.PGHost, configuration.PGPort, configuration.PGDBName, configuration.PGParams, ) default: // neznámý driver err := fmt.Errorf("driver %v is not supported", driverName) log.Err(err).Msg(canNotConnectToDataStorageMessage) return nil, err } // pokus o inicializaci připojení k databázi connection, err := sql.Open(driverName, dataSource) // test, zda bylo připojení k databázi úspěšné if err != nil { log.Err(err).Msg(canNotConnectToDataStorageMessage) return nil, err } return connection, nil }
Následují funkce, pro které v rámci dalšího textu (a navazujícího článku) postupně vytvoříme jednotkové testy. První z těchto funkcí spustí jednoduchý dotaz a zobrazí výsledky tohoto dotazu (pochopitelně s kontrolou chyb, které mohou nastat). Povšimněte si, že se jedná skutečně o jednoduchý dotaz, v němž se nepoužívají žádné parametry:
// Zobrazení všech záznamů v tabulce "persons" func displayAllRecords(connection *sql.DB) error { // dotaz do databáze query := "SELECT id, name, surname FROM persons" rows, err := connection.Query(query) // test, zda byl SQL příkaz proveden bez chyby if err != nil { return err } defer func() { // pokud dojde k chybě nebo na konci smyčky, musíme uvolnit prostředky if closeErr := rows.Close(); closeErr != nil { log.Error().Err(closeErr).Msg(unableToCloseDBRowsHandle) } }() // projít všemi vrácenými řádky for rows.Next() { var ( id int name string surname string ) // přečtení dat z jednoho vráceného řádku if err := rows.Scan(&id, &name, &surname); err != nil { return err } // výpis načteného záznamu log.Info().Int("ID", id). Str("name", name). Str("surname", surname). Msg("Record") } return nil }
Další funkce zajistí vložení nového záznamu do databáze. Povšimněte si způsobu práce s parametry, které se do dotazu předávají. Skutečné hodnoty parametrů $1 a $2 jsou předány v metodě Exec, takže se vyhneme případnému „řetězcovému“ skládání SQL dotazů, což je (nebo možná byl?) jeden z často používaných antipatternů:
// Vložení nového záznamu do tabulky "persons" func insertRecord(connection *sql.DB, name string, surname string) (int, error) { // provedení SQL příkazu se dvěma parametry sqlStatement := "INSERT INTO persons (name, surname) VALUES($1, $2);" result, err := connection.Exec(sqlStatement, name, surname) // test, zda byl SQL příkaz proveden bez chyby if err != nil { return 0, err } // přečíst počet řádků v tabulce, které byly SQL příkazem upraveny affected, err := result.RowsAffected() // i tato operace může teoreticky skončit s chybou nebo nemusí být podporována if err != nil { return 0, err } return int(affected), nil }
Třetí funkce typu CRUD (Create, read, update and delete) naopak záznam z databáze vymaže, pochopitelně za předpokladu, že je splněna zapsaná podmínka. Opět se zde využívá předávání parametrů SQL příkazu namísto skládání celého příkazu řetězcovými operacemi (což je ve většině případů zbytečné – pokud ovšem není nutné měnit jméno tabulky atd.):
// Vymazání záznamu nebo záznamů na základě zapsaného jména func deleteByName(connection *sql.DB, name string) (int, error) { // provedení SQL příkazu s jedním parametrem sqlStatement := "DELETE FROM persons WHERE name = $1;" result, err := connection.Exec(sqlStatement, name) // test, zda byl SQL příkaz proveden bez chyby if err != nil { return 0, err } // přečíst počet řádků v tabulce, které byly SQL příkazem upraveny affected, err := result.RowsAffected() // i tato operace může teoreticky skončit s chybou nebo nemusí být podporována if err != nil { return 0, err } return int(affected), nil }
Všechny tři CRUD funkce jsou otestovány po spuštění celého programu, což si ostatně ukážeme v následující kapitole:
func main() { // nastavit logovací systém pro barevný výstup na terminál log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) // konfigurace připojení do databáze config := StorageConfiguration{ Driver: "postgres", PGUsername: "postgres", PGPassword: "postgres", PGHost: "localhost", PGPort: 5432, PGDBName: "testdb", PGParams: "sslmode=disable", } log.Debug().Msg("Started") // inicializace připojení k databázi connection, err := initDatabaseConnection(config) if err != nil { log.Err(err).Msg(connectionToDBNotEstablished) return } // přečtení všech záznamů z tabulky "persons" err = displayAllRecords(connection) if err != nil { log.Err(err).Msg(databaseOperationFailed) return } // vymazání záznamu či záznamů na základě zapsaného jména affected, err := deleteByName(connection, "Eda") if err != nil { log.Err(err).Msg(databaseOperationFailed) return } log.Info().Int("deleted rows", affected).Msg("DELETE") // přidání nového záznamu do databáze affected, err = insertRecord(connection, "Eda", "Vodopád") if err != nil { log.Err(err).Msg(databaseOperationFailed) return } log.Info().Int("inserted rows", affected).Msg("INSERT") // přečtení všech záznamů z tabulky "persons" err = displayAllRecords(connection) if err != nil { log.Err(err).Msg(databaseOperationFailed) return } log.Debug().Msg("Finished") }
8. Spuštění testované aplikace
Aplikaci popsanou v předchozí kapitole spustíme, jak je tomu v ekosystému jazyka Go běžné, příkazem:
$ go run
Na standardní výstup se vypíšou (nebo by se alespoň měly vypsat) tyto řádky:
8:02PM DBG Started 8:02PM INF DB connection configuration driverName=postgres 8:02PM INF Record ID=1 name="Eliška" surname="Najbrtová" 8:02PM INF Record ID=2 name=Jenny surname=Suk 8:02PM INF Record ID=3 name="Anička" surname="Šafářová" 8:02PM INF Record ID=4 name="Sváťa" surname=Pulec 8:02PM INF Record ID=5 name="Blažej" surname="Motyčka" 8:02PM INF Record ID=6 name=Eda surname=Wasserfall 8:02PM INF Record ID=7 name="Přemysl" surname="Hájek" 8:02PM INF DELETE deleted rows=1 8:02PM INF INSERT inserted rows=1 8:02PM INF Record ID=1 name="Eliška" surname="Najbrtová" 8:02PM INF Record ID=2 name=Jenny surname=Suk 8:02PM INF Record ID=3 name="Anička" surname="Šafářová" 8:02PM INF Record ID=4 name="Sváťa" surname=Pulec 8:02PM INF Record ID=5 name="Blažej" surname="Motyčka" 8:02PM INF Record ID=7 name="Přemysl" surname="Hájek" 8:02PM INF Record ID=8 name=Eda surname="Vodopád" 8:02PM DBG Finished

Obrázek 1: Skutečný výstup na terminálu po spuštění testované aplikace.
Současně dojde k automatické modifikaci souboru go.mod, protože se doplní potřebné knihovny:
module db-test go 1.14 require ( github.com/lib/pq v1.10.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/rs/zerolog v1.20.0 )
Vytvoří se i soubor go.sum s tranzitivními závislostmi:
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Nakonec se můžeme po přihlášení do interaktivní konzole psql přesvědčit, že došlo ke změně obsahu tabulky:
$ psql -U postgres Password for user postgres: (zde se taktéž použije nastavené heslo) psql (9.6.10) Type "help" for help. postgres=# postgres=# \c testdb testdb=# select * from persons; id | name | surname ----+---------+----------- 1 | Eliška | Najbrtová 2 | Jenny | Suk 3 | Anička | Šafářová 4 | Sváťa | Pulec 5 | Blažej | Motyčka 7 | Přemysl | Hájek 8 | Eda | Vodopád (7 rows)
9. Jednotkové testy v programovacím 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 všech nových informačních systémů (a ostatně i desktopových aplikací). I z tohoto důvodu se v nabídce standardních nástrojů programovacího 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, výpočtem procenta pokrytí kódu testy 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 relativně snadno 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) a v případě SQL databází pak balíčkem go-sqlmock.
Jak jsme si již řekli v úvodním odstavci této kapitoly, 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ů – tedy 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 tedy 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 poměrně velké množství metod pro strukturu testing.T, které jsou použity právě při tvorbě jednotkových testů. Z praktického hlediska se jedná především o následující 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 |
10. Příklad vytvoření jednotkových testů
Již v dokumentaci k programovacímu jazyku Go je ve stručnosti 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 v této kapitole podrobněji. Nejprve se podívejme na zdrojový kód obsahující funkci nazvanou Add, kterou budeme chtít otestovat s využitím jednotkových testů. 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í.
Ukažme si tedy způsob naprogramování velmi jednoduchého jednotkového testu určeného pro otestování funkcionality funkce Add. Použijeme přitom takový postup, že se zavoláním metody 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") } }
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
Dále je možné zjistit pokrytí kódu jednotkovými testy:
$ go test -v -coverprofile coverage.out
Vygenerovaný soubor coverage.out slouží pro další analýzy.
11. 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 jednotkových 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. Dnes se seznámíme s jednou knihovnou určenou primárně pro mockování funkcí a metod používaných SQL drivery pro přístup k relačním databázím. Tato knihovna umožňuje testovat aplikace používající jak přímo standardní balíček database/sql, tak i prakticky jakýkoli balíček nabízející ORM, tedy objektově-relační mapování.
12. Mockování funkcí a metod přistupujících do relační databáze s využitím knihovny go-sqlmock
Pro otestování funkcí, které přistupují do relační databáze, použijeme knihovnu nazvanou go-sqlmock. Tato knihovna nabízí aplikacím vlastní SQL driver, takže ji lze použít jak pro ty aplikace, které přímo volají funkce a metody z balíčku database/sql, tak pro aplikace postavené na ORM, například na knihovně Gorm. Samotná struktura testů je následující:
- Vytvoří se připojení do mockované databáze, což je objekt typu *sql.DB
- SQL driver se nakonfiguruje – specifikuje se, jaký dotaz či dotazy má očekávat
- Dále se specifikuje, jaká data se mají vrátit jako výsledek dotazu (tedy mockujeme odpověď databáze)
- Zavolá se testovaná funkce, která interně volá SQL příkazy
- Otestuje se, zda jsou splněny podmínky z bodu 2, tedy zda se skutečně zavolaly očekávané SQL příkazy a jen tyto příkazy
- Otestuje se, zda volaná funkce vrátila očekávaná data (typicky získaná z dat vrácených mockovanou databází)
13. Export testovaných funkcí a kostra jednotkových testů
Nyní již máme k dispozici všechny základní informace o způsobu testování kódu, který používá balíček database/sql, a to jak přímo, tak i nepřímo. Můžeme se tedy pokusit vytvořit si první jednoduché jednotkové testy. Nejprve je nutné zajistit, aby byly testované funkce dostupné (viditelné) nejenom z balíčku main, ale i z balíčku main_test. K tomu můžeme použít malého triku – vytvoříme soubor export_test.go, který je v balíčku main a který bude obsahovat alternativní jména testovaných funkcí, ovšem začínajících na velké písmeno (tedy takové funkce budou viditelné i vně balíčku):
package main var ( DisplayAllRecords = displayAllRecords )
Dále můžeme vytvořit kostru jednotkových testů. Bude se jednat o soubor se jménem končícím na _test.go a bude spadat do balíčku main_test (tím pádem se testy nebudou linkovat do výsledného binárního kódu aplikace). Importovat budeme pochopitelně balíček testing zmíněný v předchozích kapitolách, dále hlavní balíček main a taktéž balíček go-sqlmock:
package main_test import ( "testing" main "db-test" "github.com/DATA-DOG/go-sqlmock" )
14. Základní otestování funkce DisplayAllRecords
Nyní již můžeme vytvořit jednotkový test, který bude volat funkci DisplayAllRecords (resp. původní funkci displayAllRecords). Hlavička funkce s jednotkovým testem vypadá následovně – jméno funkce začíná na Test a jediný parametr je typu *testing.T:
func TestSelect1(t *testing.T) {
V testu nejdříve vytvoříme připojení do databáze, ovšem nyní nikoli do reálné databáze PostgreSQL; namísto toho použijeme SQL mock:
connection, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) }
Získaný objekt se chová stejně jako reálný SQL driver, tudíž musíme zajistit uzavření připojení na konci (teoreticky nemusíme, ale takto je celý přístup čistější a mocky se nebudou hromadit v operační paměti):
defer connection.Close()
Nyní konečně nastává vlastní příprava mocku. Vytvoříme datovou strukturu s popisem řádků, které má mockované SQL vrátit. Prozatím budeme požadovat, aby se nevrátily žádné záznamy, tudíž struktura sice bude existovat, ale bude prázdná:
rows := sqlmock.NewRows([]string{})
Nejdůležitější je následující řádek, kterým se specifikuje, jaký dotaz bude SQL mock očekávat (tedy jak se má chovat testovaná funkce) a jaká data se testované funkci vrátí jako výsledek SQL dotazu. Celý zápis je možné zapsat na jediný řádek:
mock.ExpectQuery("SELECT id, name, surname FROM persons").WillReturnRows(rows)
SQL mock je připraven a tudíž se můžeme pokusit zavolat testovanou funkci, které přidáme objekt realizující připojení do mockovaného SQL driveru:
err = main.DisplayAllRecords(connection) if err != nil { t.Errorf("error was not expected while updating stats: %s", err) }
Pokud testovaná funkce nevrátila chybu, ještě to vůbec neznamená, že pracuje s SQL korektně. To zjistíme až otestováním, zda mockované SQL přijalo očekávaný dotaz (a nikoli dotaz jiný či žádný):
err = mock.ExpectationsWereMet() if err != nil { t.Errorf("there were unfulfilled expectations: %s", err) }
Výsledná podoba funkce s jednotkovým testem bude vypadat následovně:
func TestSelect1(t *testing.T) { connection, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer connection.Close() rows := sqlmock.NewRows([]string{}) mock.ExpectQuery("SELECT id, name, surname FROM persons").WillReturnRows(rows) err = main.DisplayAllRecords(connection) if err != nil { t.Errorf("error was not expected while updating stats: %s", err) } err = mock.ExpectationsWereMet() if err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }
15. Přidání řádků, které SQL mock vrátí testované funkci jako výsledek SQL dotazu
Mockovaný driver SQL databáze dokáže z SQL dotazu (typicky SELECT) vrátit libovolná v jednotkových testech specifikovaná data. Postačuje pouze tato data reprezentovat formou datové struktury typu sqlmock.Rows. Podívejme se nyní na celý postup, který není složitý.
Strukturu sqlmock.Rows zkonstruujeme s využitím konstruktoru sqlmock.NewRows; přitom je nutné specifikovat jména sloupců výsledku (povšimněte si, že se neřeší struktura zdrojových tabulek, ale pouze výsledek dotazu):
rows := sqlmock.NewRows([]string{"id", "name", "surname"})
Do této struktury se nové řádky přidávají metodou nazvanou AddRow. Opět si povšimněte, že lze relativně bez problémů pracovat s různými datovými typy prvků (poněkud problematická je práce například s časovými razítky, o čemž se zmíníme příště):
rows.AddRow(1, "foo", "bar") rows.AddRow(2, "x", "y") rows.AddRow(3, "a", "b")
Předání dat do mockovaného driveru:
mock.ExpectQuery("SELECT id, name, surname FROM persons").WillReturnRows(rows)
A to je vše – upravený test tedy může vypadat následovně:
func TestSelect2(t *testing.T) { connection, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer connection.Close() rows := sqlmock.NewRows([]string{"id", "name", "surname"}) rows.AddRow(1, "foo", "bar") rows.AddRow(2, "x", "y") rows.AddRow(3, "a", "b") mock.ExpectQuery("SELECT id, name, surname FROM persons").WillReturnRows(rows) err = main.DisplayAllRecords(connection) if err != nil { t.Errorf("error was not expected while updating stats: %s", err) } err = mock.ExpectationsWereMet() if err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }
Nyní po spuštění jednotkových testů můžeme na standardním výstupu vidět, že funkce displayAllRecords skutečně získá trojici řádků, jejichž obsah vypíše na obrazovku:
$ go test {"level":"info","ID":1,"name":"foo","surname":"bar","time":"2021-03-16T15:05:10+01:00","message":"Record"} {"level":"info","ID":2,"name":"x","surname":"y","time":"2021-03-16T15:05:10+01:00","message":"Record"} {"level":"info","ID":3,"name":"a","surname":"b","time":"2021-03-16T15:05:10+01:00","message":"Record"}
16. Test funkce vracející data načtená z databáze
Pokusme se nyní upravit naši aplikaci takovým způsobem, aby obsahovala funkci, která načte záznamy z databáze a vrátí tato data formou řezu záznamů. Bude se tedy jednat o poměrně snadno testovatelnou funkci (a z hlediska návrhu aplikace lepší funkci s jasně definovaným jediným úkolem).
Nejprve si připravíme strukturu reprezentující každý záznam přečtený z databáze:
// datová struktura odpovídající struktuře záznamu v databázi type Record struct { Id int Name string Surname string }
Samotná funkce pro přečtení záznamů z databáze (se všemi potřebnými testy na případné chyby) může vypadat následovně:
// funkce vracející data přečtená z databázové tabulky func readAllRecords(connection *sql.DB) ([]Record, error) { results := make([]Record, 0) // dotaz do databáze query := "SELECT id, name, surname FROM persons" rows, err := connection.Query(query) // test, zda byl SQL příkaz proveden bez chyby if err != nil { return results, err } defer func() { // pokud dojde k chybě nebo na konci smyčky, musíme uvolnit prostředky if closeErr := rows.Close(); closeErr != nil { log.Error().Err(closeErr).Msg(unableToCloseDBRowsHandle) } }() // projít všemi vrácenými řádky for rows.Next() { var record Record // přečtení dat z jednoho vráceného řádku if err := rows.Scan(&record.Id, &record.Name, &record.Surname); err != nil { return results, err } results = append(results, record) } return results, nil }
Před úpravou testů musíme do souboru export_test.go přidat jmenný alias existující funkce, ovšem s velkým písmenem na začátku:
package main var ( DisplayAllRecords = displayAllRecords ReadAllRecords = readAllRecords )
Do souboru s jednotkovými testy nyní můžeme přidat nový test:
func TestSelect3(t *testing.T) { connection, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer connection.Close() rows := sqlmock.NewRows([]string{"id", "name", "surname"}) rows.AddRow(1, "foo", "bar") rows.AddRow(2, "x", "y") rows.AddRow(3, "a", "b") mock.ExpectQuery("SELECT id, name, surname FROM persons").WillReturnRows(rows) results, err := main.ReadAllRecords(connection) if err != nil { t.Errorf("error was not expected while updating stats: %s", err) } err = mock.ExpectationsWereMet() if err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } if len(results) != 3 { t.Errorf("different number of results read from database: %d instead of 3", len(results)) return } expected := main.Record{1, "foo", "bar"} if results[0] != expected { t.Errorf("first result is different: %+v versus %+v", results[0], expected) } }
Pro zajímavost se podívejme na výsledek testů a kontrolu pokrytí zdrojového kódu testy:
$ go test -timeout 2m -coverprofile coverage.out {"level":"info","ID":1,"name":"foo","surname":"bar","time":"2021-03-16T15:05:10+01:00","message":"Record"} {"level":"info","ID":2,"name":"x","surname":"y","time":"2021-03-16T15:05:10+01:00","message":"Record"} {"level":"info","ID":3,"name":"a","surname":"b","time":"2021-03-16T15:05:10+01:00","message":"Record"} PASS coverage: 11.0% of statements ok db-test 0.003s $ go tool cover -func=coverage.out db-test/db_operations.go:37: initDatabaseConnection 0.0% db-test/db_operations.go:80: displayAllRecords 76.9% db-test/db_operations.go:127: readAllRecords 0.0% db-test/db_operations.go:162: insertRecord 0.0% db-test/db_operations.go:183: deleteByName 0.0% db-test/db_operations.go:203: main 0.0% total: (statements) 11.0%

Obrázek 2: Vizuální znázornění pokrytí zdrojového kódu jednotkovými testy (stále se používají standardní nástroje jazyka Go).
17. Funkce se složitějším SQL dotazem s klauzulí WHERE
V dalším kroku do naší testované aplikace přidáme další funkci. I tato funkce bude volat SQL dotaz a zpracovávat výsledky tohoto dotazu, ovšem nyní bude dotaz obsahovat parametr, konkrétně jméno, které mají vyhledávané záznamy obsahovat (ostatní kód je podobný předchozí funkci, ale pro větší přehlednost prozatím neprovedeme refaktoring):
// funkce vracející data přečtená z databázové tabulky func readRecordsWithName(connection *sql.DB, name string) ([]Record, error) { results := make([]Record, 0) // dotaz do databáze query := "SELECT id, name, surname FROM persons WHERE name=$1" rows, err := connection.Query(query, name) // test, zda byl SQL příkaz proveden bez chyby if err != nil { return results, err } defer func() { // pokud dojde k chybě nebo na konci smyčky, musíme uvolnit prostředky if closeErr := rows.Close(); closeErr != nil { log.Error().Err(closeErr).Msg(unableToCloseDBRowsHandle) } }() // projít všemi vrácenými řádky for rows.Next() { var record Record // přečtení dat z jednoho vráceného řádku if err := rows.Scan(&record.Id, &record.Name, &record.Surname); err != nil { return results, err } results = append(results, record) } return results, nil }
18. Otestování nové funkce
Otestování nové funkce přidané v rámci sedmnácté kapitoly bude triviální. Nejdříve jméno této funkce přidáme do souboru export_test.go:
package main var ( DisplayAllRecords = displayAllRecords ReadAllRecords = readAllRecords ReadRecordsWithName = readRecordsWithName )
Dále vytvoříme jednotkový test. Zde si povšimněte způsobu zápisu příkazu, který je očekáván (tedy který má testovaná funkce zavolat). V příkazu se před parametr zadává zpětné lomítko, což ovšem nemusí být na první pohled zřejmé:
mock.ExpectQuery("SELECT id, name, surname FROM persons WHERE name=\\$1").WillReturnRows(rows)
Samotná implementace jednotkového testu:
func TestSelect4(t *testing.T) { connection, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer connection.Close() rows := sqlmock.NewRows([]string{"id", "name", "surname"}) rows.AddRow(2, "Eda", "Vodopád") mock.ExpectQuery("SELECT id, name, surname FROM persons WHERE name=\\$1").WillReturnRows(rows) results, err := main.ReadRecordsWithName(connection, "Eda") if err != nil { t.Errorf("error was not expected while updating stats: %s", err) } err = mock.ExpectationsWereMet() if err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } if len(results) != 1 { t.Errorf("different number of results read from database: %d instead of 1", len(results)) return } expected := main.Record{2, "Eda", "Vodopád"} if results[0] != expected { t.Errorf("first result is different: %+v versus %+v", results[0], expected) } }
Ve skutečnosti je však možné očekávaný dotaz zapsat ve formě regulárního výrazu:
mock.ExpectQuery("SELECT id, name, surname FROM persons WHERE name=.*").WillReturnRows(rows)
Výsledný test:
func TestSelect5(t *testing.T) { connection, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer connection.Close() rows := sqlmock.NewRows([]string{"id", "name", "surname"}) rows.AddRow(2, "Eda", "Vodopád") mock.ExpectQuery("SELECT id, name, surname FROM persons WHERE name=.*").WillReturnRows(rows) results, err := main.ReadRecordsWithName(connection, "Eda") if err != nil { t.Errorf("error was not expected while updating stats: %s", err) } err = mock.ExpectationsWereMet() if err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } if len(results) != 1 { t.Errorf("different number of results read from database: %d instead of 1", len(results)) return } expected := main.Record{2, "Eda", "Vodopád"} if results[0] != expected { t.Errorf("first result is different: %+v versus %+v", results[0], expected) } }
Výsledek spuštění jednotkových testů s výpočtem pokrytí kódu testy:
db-test/db_operations.go:37: initDatabaseConnection 0.0% db-test/db_operations.go:80: displayAllRecords 76.9% db-test/db_operations.go:128: readAllRecords 78.6% db-test/db_operations.go:163: readRecordsWithName 78.6% db-test/db_operations.go:198: insertRecord 0.0% db-test/db_operations.go:219: deleteByName 0.0% db-test/db_operations.go:239: main 0.0% total: (statements) 28.6% {"level":"info","ID":1,"name":"foo","surname":"bar","time":"2021-03-16T15:47:35+01:00","message":"Record"} {"level":"info","ID":2,"name":"x","surname":"y","time":"2021-03-16T15:47:35+01:00","message":"Record"} {"level":"info","ID":3,"name":"a","surname":"b","time":"2021-03-16T15:47:35+01:00","message":"Record"} PASS coverage: 28.6% of statements ok db-test 0.003s
Dalším krokem tedy bude otestování zbylého kódu (což jsou většinou kontroly chyb) a taktéž CRUD příkazů INSERT a DELETE. To je ovšem téma, kterému se budeme věnovat příště.
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového 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ě stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
# | Příklad/soubor | Stručný popis | Cesta |
---|---|---|---|
1 | go.mod | projektový soubor projektu psaného v Go | https://github.com/tisnik/go-root/blob/master/article72/go.mod |
2 | go.sum | závislosti vytvářeného projektu | https://github.com/tisnik/go-root/blob/master/article72/go.sum |
3 | db_operations.go | testovaná aplikace s několika databázovými operacemi | https://github.com/tisnik/go-root/blob/master/article72/db_operations.go |
4 | db_operations_test.go | jednotkové testy založené na mockování SQL příkazů | https://github.com/tisnik/go-root/blob/master/article72/db_operations_test.go |
5 | export_test.go | export jmen funkcí volaných v jednotkových testech | https://github.com/tisnik/go-root/blob/master/article72/export_test.go |
20. Odkazy na Internetu
- ORM je antipattern
https://zdrojak.cz/clanky/orm-je-antipattern/ - DATA-DOG / go-sqlmock repository
https://github.com/DATA-DOG/go-sqlmock - Sql driver mock for Golang
https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock?utm_source=godoc - The fantastic ORM library for Golang
http://gorm.io/ - Package sql
https://golang.org/pkg/database/sql/ - Go database/sql tutorial
http://go-database-sql.org/ - SQLDrivers
https://github.com/golang/go/wiki/SQLDrivers - Package testing
https://golang.org/pkg/testing/ - Golang basics – writing unit tests
https://blog.alexellis.io/golang-writing-unit-tests/ - An Introduction to Programming in Go / Testing
https://www.golang-book.com/books/intro/12 - An Introduction to Testing in Go
https://tutorialedge.net/golang/intro-testing-in-go/ - Advanced Go Testing Tutorial
https://tutorialedge.net/golang/advanced-go-testing-tutorial/ - GoConvey
http://goconvey.co/ - Testing Techniques
https://talks.golang.org/2014/testing.slide - 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 - Dokumentace k balíčku oglematchers
https://godoc.org/github.com/jacobsa/oglematchers - Balíček oglematchers
https://github.com/jacobsa/oglematchers - Dokumentace k balíčku ogletest
https://godoc.org/github.com/jacobsa/ogletest - Balíček ogletest
https://github.com/jacobsa/ogletest - Dokumentace k balíčku assert
https://godoc.org/github.com/stretchr/testify/assert - Testify – Thou Shalt Write Tests
https://github.com/stretchr/testify/ - PostgreSQL CREATE DATABASE
https://www.postgresqltutorial.com/postgresql-create-database/ - PostgreSQL CREATE SCHEMA
https://www.postgresqltutorial.com/postgresql-create-schema/ - PostgreSQL CREATE TABLE
https://www.postgresqltutorial.com/postgresql-create-table/ - PostgreSQL SELECT
https://www.postgresqltutorial.com/postgresql-select/ - PostgreSQL INSERT
https://www.postgresqltutorial.com/postgresql-insert/ - PostgreSQL DELETE
https://www.postgresqltutorial.com/postgresql-delete/ - Data definition language
https://en.wikipedia.org/wiki/Data_definition_language - Data query language
https://en.wikipedia.org/wiki/Data_query_language - Data manipulation language
https://en.wikipedia.org/wiki/Data_manipulation_language