Defekty patria neodmysliteľne k písaniu kódu. V knihe Code Complete Steve McConnell uvádza priemyselný štandard 15-50 defektov na 1000 riadkov kódu (KLOC) v teste a 5 defektov na KLOC v produkcii s nákladmi na testovanie $5 na riadok kódu. Microsoft aplikáciach vstupuje do testov 10-20 defektov na KLOC a do produkcie 0,5 defektov na KLOC. Defekty môžu mať za následok stratu života alebo veľké finančné straty. Koľko defektov je príliš veľa? NASA sa snaží o dosiahnutie 0 defektov na KLOC, avšak za cenu tisícov USD na jeden riadok kódu (podľa reportov 0,004 defektov/KLOC stojí NASA $850 na riadok kódu). Preto je dôležité hustotu defektov vo fáze testovania a v produkcii merať a na začiatku projektu si vzhľadom na dopady stanoviť kritéria, rozpočet a nastaviť si ciele na zlepšovanie.
V tomto článku sa zameriame na dve témy - ako ošetrovať a spracovávať chyby a testovanie pomocou Go toolingu.
Spracovanie chýb
Predstavme si ilustračný kód s funkciou divide
, ktorá berie dva vstupné parametre a vráti výsledok delenia. Aký je problém s týmto kódom?
|
|
Ak zavoláme divide(5, 0)
tak program v panike skolabuje s hláškou panic: runtime error: integer divide by zero
V iných jazykoch je problém obyčajne riešný vyhodením výnimky (Exception objekt), ktorú treba odchytiť (catch) na vyššej úrovni alebo program rovnako skolabuje. V niektorých jazykoch zo signatúry funkcie nevieme povedať, či hádže výnimky (C++), v iných môžu alebo nemusia byť jasné zo signatúry (unchecked - java.Error, java.RuntimeException). Druhý problém, ktorý nastáva, je že nie každá chyba je výnimka a v situáciach, keď chceme vrátiť prázdnu hodnotu musíme návratovú hodnotu obaliť do objektu a neskôr rozbaliť. To je riešené napríklad Maybe alebo Optional monádou vo funkcionálnom programovaní (ktorého som veľkým fanúšikom), alebo int?
v C#, ale nerieši to napríklad návratové chybové kódy a správy.
Go rieši výnimky tak, že ich nepozná. Umožňuje však vrátenie viac ako jedného parametra a pozná rozhranie typu error
alebo vo fatálnom prípade panic(message)
- napríklad ak došla RAM a program sa nemôže spamätať (pričom z paniky sa stále máme možnosť spamätať prostredníctvom recover
v defer
bloku - viac info tu).
Dôvodom je prehľadnosť kontrolného toku programu a čitateľnosť kódu. Neexistuje neviditeľný neprehľadný tok výnimiek spracovávaných na roznych úrovniach. Buď chybu na mieste spracujeme, alebo nie. Dôvody tohto dizajnu si možete prečítať vo FAQ. Niektorí kritizujú, že tento štýl zväčšuje počet napísaných riadkov kódu, iní to vnímajú ako úľavu. Pamätajme, že kód je niekoľkonásobne viac čítaný ako písaný…
Ako by teda náš kód vyzeral po ošetrení?
|
|
errors.New
vytvorí pri delení nulou typ error s hláškou a vráti. Prvá návratová hodnota musí byť stále typu int
preto musíme vrátiť číslo. Iný spôsob ak chceme vrátiť formátovaný error je zmeniť riadok na return 0, fmt.Errorf("divide: cannot divide %v by zero", a)
.
A čo keď si chceme zadefinovať vlastné chyby s návratovými hodnotami? Tu už ideme trochu hlbšie. Vieme, že chyba v Go je rozhranie typu error
. Error rozhranie je deklarované takto:
type error interface {
Error() string
}
To znamená, že akákoľvek dátová štruktúra, ktorá akceptuje funkciu Error()
vracajúcu string
môže byť automaticky použitá ako error. Takto by vyzeral náš kód ak by sme chceli vracať aj chybový kód:
|
|
Testovanie pomocou Go toolingu
Go obsahuje jednoduchý ale prepracovaný tooling na testovanie.
Unit a benchmark testy si môžeme vytvoriť v osobitnom súbore s názvom *_test.go
, napríklad pkg_test.go.
V tomto súbore testov importneme balíček testing
. Každá testovacia funkcia musí mať tvar func TestXXX(t *testing.T)
, respektíve func BenchmarkXXX(t *testing.B)
pre benchmark testy (XXX je ľubovoľný názov testu).
Testy spustíme príkazom go test
, ktorý podporuje viacero flagov.
Kód nižšie obsahuje jeden pozitívny prípad, jeden negatívny a jeden benchmark test:
|
|
Ak potrebujeme urobiť setup a teardown testov pred akýmkoľvek testom, tak môžeme použiť funckiu TestMain
. Ak testovací súbor obsahuje túto funkciu tak sa nezavolá žiaden test, ale len funkcia TestMain
a m.Run()
zavolá všetky testy daného súboru. Ak test zlyhal, tak os.Exit
vráti nenulovú hodnotu. setup()
a shutdown()
sú ľubovolné funkcie aké si definujeme.
|
|
Ak chceme setup a teardown na úrovni špecifického testu, tak sa odporúča volať setup a teardown funkcie v tele testu.
Nákladnejší setup v benchmark testoch ovplivní performance testu, preto po setupe benchmark testu by sme mali resetovať časovač funkciou b.ResetTimer()
.
Testovanie je podstatne komplexnejšia oblasť, to čo sme si ukázali sú len základné unit testy. Testy môžeme zjednodušiť a obohatiť o assert, require, mocky a suity prostredníctvom populárneho balíčku Testify.
Výstupy
Spustenie všetkých funkčných testov:
$ go test
PASS
ok
Spustenie všetkých funkčných testov s vypísaním podmienených log správ:
$ go test -v
=== RUN TestDivide
--- PASS: TestDivide (0.00s)
pkg_test.go:12: 2
=== RUN TestDivideZero
--- PASS: TestDivideZero (0.00s)
pkg_test.go:20: 0
PASS
ok
Spustenie všetkých performance benchmark testov:
$ go test -bench .
goos: windows
goarch: amd64
BenchmarkDivide-24 1000000000 0.464 ns/op
PASS
ok
Referencie
- Error handling and Go
- Working with Errors in Go 1.13 - wrap/unwrap
- Errors are values
- Package testing
- Improving Your Go Tests and Mocks With Testify
- Basic testing patterns in Go
Vaše otázky, návrhy a komentáre
Verím, že vás tento návod inšpiroval a budem vďačný ak dáte spätnú väzbu a pomôže mi zamerať sa na to čo by vás zaujímalo.