Грешки, тестове и документация

21.11.2018

В този епизод:

Но преди това...

Въпрос за мъфин #1

Какво прави select?

Error handling

Имало едно време чисто С

Пример в C

#include <stdio.h>
#include <errno.h>
#include <string.h>

extern int errno;

int main ()
{
    FILE* pf = fopen("unexist.txt", "rb");
    if (pf == NULL) {
        fprintf(stderr, "Value of errno: %d\n", errno);
        perror("Error printed by perror");
        fprintf(stderr, "Error opening file: %s\n", strerror(errno));
    }
    else {
        fclose(pf);
    }
    return 0;
}

Имало едно време един език Go

Има грубо-казано 2 начина

Връщане на грешка

type error interface {
    Error() string
}
type PathError struct {
    Op string    // "open", "unlink", etc.
    Path string  // Файлът с грешката
    Err error    // Грешката, върната от system call-a
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

Стандартна употреба

func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    //...
}

или малко по-сложно:

func CreateFile(filename string) (*os.File, error) {
    var file, err = os.Create(filename)
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles() // Free some space
        return os.Create(filename)
    }
    return file, err
}

Errors are values

Често оплакване на Go програмисти е количеството проверки за грешки:

if err != nil {
    return err
}

Пример

if _, err := fd.Write(p0[a:b]); err != nil {
    return err
}
if _, err := fd.Write(p1[c:d]); err != nil {
    return err
}
if _, err := fd.Write(p2[e:f]); err != nil {
    return err
}

Може да стане:

var err error
write := func(buf []byte) {
    if err == nil {
        _, err = w.Write(buf)
    }
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
if err != nil {
    return err
}

Създаване на грешки

func someFunc(a int) (someResult, error) {
    if a <= 0 {
        return nil, errors.New("a must be positive!")
    }
    // ...
}
func someFunc(a int) (someResult, error) {
    if a <= 0 {
        return nil, fmt.Errorf("a is %d, but it must be positive!", a)
    }
    // ...
}

Припомняне на defer

func main() {
    fmt.Println("start")
    defer fmt.Println("first")

    if false { // ex. try to open a file
        fmt.Println("error")
        return
    }
    defer fmt.Println("second")

    fmt.Println("done")
}

Паника!

Уточнения

Избягвайте ненужното изпадане в паника

recover

Example

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}
func g(i int) {
    if i > 2 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}
func main() {
    f()
    fmt.Println("Returned normally from f.")
}

Грешки в "Go 2"

Go 2?!
До сега не сме ви говорили за Go2, така че настанете се удобно...

Go 2

Грешки в "Go 2"

Има две интересни секции в черновата за Go2 предложение:

Тестове и документация

Disclamer

Днес няма да си говорим за acceptance testing, quality assurance или нещо, което се прави от QA отдела във фирмата.

Всичко тук е дело на програмиста.

Митът

Проектът идва с готово, подробно задание.

Прави се дизайн.

С него работата се разбива на малки задачи.

Те се извършват последователно.

За всяка от тях пишете кода и приключвате.

Изискванията не се променят, нито се добавя нова функционалност.

Митът v2.0

Щом съм написал един код, значи ми остава единствено да го разцъкам - няколко print-а, малко пробване в main функцията и толкова.

Така или иначе няма да се променя.

А ако (не дай си боже) това се случи - аз съм го писал, знам го, няма как да допусна грешка.

Най-много да го поразцъкам още малко.

Тежката действителност

Заданията винаги се променят.

Често се налага един код да се преработва.

Писането на код е сложна задача - допускат се грешки.

Програмистите са хора - допускат грешки.

Промяната на модул в единия край на системата като нищо може да счупи модул в другия край на системата.

Идва по-добра идея за реализация на кода, по ред причини.

Искаме да автоматизираме нещата

За всичко съмнително ще пишем сценарий, който да "цъка".

Всеки сценарий ще изпълнява кода и ще прави няколко твърдения за резултатите.

Сценариите ще бъдат обединени в групи.

Пускате всички тестове с едно бутонче.

Резултатът е "Всичко мина успешно" или "Твърдения X, Y и Z в сценарии A, B и C се оказаха неверни".

Искаме да тестваме и производителността на нашия код.

Видове тестове

За какво ни помагат тестовете

За какво не служат тестовете

testing

Разбрахме се, че тестовете са ни супер важни.

Очевидно в стандартната библиотека на Go, има пакет за това.

За да тестваме foo.go, създаваме foo_test.go в същата директория, който тества foo.go

Ако тестваме пакета foo можем:

С go test ./... пускаме тестовете на един цял проект :)

Тестовете в testing

func TestFibonacciFastest(t *testing.T) {
    n := FibonacciFastest(0)
    if n != 1 {
        t.Error("FibonnaciFastest(0) returned" + n + ", we expected 1")
    }
}

Оркестрация на тестове

Benchmark тестове

func BenchmarkFibonacciFastest(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibonacciFastest(40)
    }
}

Demo

Малко почивка с тестовете...

Документиране на кода

go генерира автоматична документация на нашия код, вземайки под внимание:

/*
    Example fobonacci numbers implementation
*/
package fibonacci
// Fastest Fibonacci Implementation
func FibonacciFastest(n uint64) uint64 {
// lookupTable stores computed results from FibonacciFast or FibonacciFastest.
var lookupTable = map[uint64]uint64{}

Виждане на документацията

На всички локално инсталирани пакети

godoc -http=:6060

Документация на (почти) всички go пакети качени в BitBucket, GitHub, Launchpad и Google Project Hosting

The testing must go on...

Сечението на testing.B и testing.T

func testingFunc(tb testing.TB, input, expectedResult int) {
    if result := Fibonacci(input); result != expectedResult {
        tb.Fatalf("Expected %d for Fiboncci(%d) but got %d", expectedResult, input, result)
    }
}

Таблично базирани тестове

func TestFibonacci(t *testing.T) {
    var tests = []struct {
        input, result int
    }{
        {input: 2, result: 1},
        {input: 8, result: 21},
        {input: 10, result: 55},
    }
    for _, test := range tests {
        testingFunc(t, test.input, test.result)
    }
}

SubTests

func TestSubFibonacci(t *testing.T) {
    var tests = []struct {
        input, result int
    }{
        {input: 2, result: 1},
        {input: 8, result: 21},
        {input: 10, result: 55},
    }
    for _, test := range tests {
        t.Run(fmt.Sprintf("Fibonacci(%d)", test.input), func(t *testing.T) {
            testingFunc(t, test.input, test.result)
        })
    }
}

SubTests continues

--- FAIL: TestFibonacci (0.00s)
        table_test.go:68: Expected 1 for Fiboncci(2) but got 21
--- FAIL: TestSubFibonacci (0.00s)
    --- FAIL: TestSubFibonacci/Fibonacci(2) (0.00s)
        table_test.go:68: Expected 1 for Fiboncci(2) but got 21
    --- FAIL: TestSubFibonacci/Fibonacci(10) (0.00s)
        table_test.go:68: Expected 55 for Fiboncci(10) but got 21
FAIL

SubBenchmarks

func BenchmarkSubFibonacci(b *testing.B) {
    var tests = []struct {
        input, result int
    }{
        {input: 2, result: 1},
        {input: 8, result: 21},
        {input: 10, result: 55},
    }
    for _, test := range tests {
        b.Run(fmt.Sprintf("BFibonacci(%d)", test.input), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                testingFunc(b, test.input, test.result)
            }
        })
    }
}

SubBenchmarks output:

--- FAIL: BenchmarkSubFibonacci/BFibonacci(2)
        table_test.go:68: Expected 1 for Fiboncci(2) but got 21
BenchmarkSubFibonacci/BFibonacci(8)-16          1000000000               2.65 ns/op
--- FAIL: BenchmarkSubFibonacci/BFibonacci(10)
        table_test.go:68: Expected 55 for Fiboncci(10) but got 21
--- FAIL: BenchmarkSubFibonacci
FAIL

All together

func TestGroupSubFibonacci(t *testing.T) {
    t.Run("group1", func(t *testing.T) {
        for _, test := range tests {
            t.Run(fmt.Sprintf("Fibonacci(%d)", test.input), func(t *testing.T) {
                var input, result = test.input, test.result
                t.Parallel()
                time.Sleep(time.Second)
                testingFunc(t, input, result)
            })

        }
        t.Run("NonParallel", func(t *testing.T) {
            t.Fatal("Just cause")
        })
        t.Fatal("Oops")
        t.Run("NonParallel2", func(t *testing.T) {
            t.Fatal("Just Cause II")
        })
    })
}

All together output

--- FAIL: TestGroupSubFibonacci (1.00s)
    --- FAIL: TestGroupSubFibonacci/group1 (0.00s)
        --- FAIL: TestGroupSubFibonacci/group1/NonParallel (0.00s)
                table_test.go:96: Just cause
        table_test.go:98: Oops
        --- FAIL: TestGroupSubFibonacci/group1/Fibonacci(2) (1.00s)
                table_test.go:69: Expected 1 for Fiboncci(2) but got 21
        --- FAIL: TestGroupSubFibonacci/group1/Fibonacci(10) (1.00s)
                table_test.go:69: Expected 55 for Fiboncci(10) but got 21
FAIL

Example тестове - шантавата част

Foo -> ExampleFoo
func ExampleHello() {
    Hello("hello")
    // Output:
    // hello
}

Имитации

Иматации или Mock-ове

В Go

type Sleeper interface {
   Sleep(time.Duration)
}

Библиотеки за имитации

testify

mockery

mock

Въпроси?