Показывает охват функциональных тестов без слепых зон

У меня есть рабочий код golang и функциональные тесты для него, написанные не на golang. Функциональные тесты запускают скомпилированный двоичный файл. Очень упрощенная версия моего производственного кода находится здесь: main.go:

package main

import (
    "fmt"
    "math/rand"
    "os"
    "time"
)

func main() {
    rand.Seed(time.Now().UTC().UnixNano())
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            os.Exit(0)
        }
        if i%2 == 0 {
            os.Exit(1)
        }
        time.Sleep(time.Second)
    }
}

Я хочу построить профиль покрытия для моих функциональных тестов. Для этого добавляю main_test.go файл с содержанием:

package main

import (
    "os"
    "testing"
)

var exitCode int

func Test_main(t *testing.T) {
    go main()
    exitCode = <-exitCh
}

func TestMain(m *testing.M) {
    m.Run()
    // can exit because cover profile is already written
    os.Exit(exitCode)
}

И изменить main.go:

package main

import (
    "flag"
    "fmt"
    "math/rand"
    "os"
    "runtime"
    "time"
)

var exitCh chan int = make(chan int)

func main() {
    rand.Seed(time.Now().UTC().UnixNano())
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            exit(0)
        }
        if i%2 == 0 {
            fmt.Println("status 1")
            exit(1)
        }
        time.Sleep(time.Second)
    }
}

func exit(code int) {
    if flag.Lookup("test.coverprofile") != nil {
        exitCh <- code
        runtime.Goexit()
    } else {
        os.Exit(code)
    }
}

Затем я строю покрытие двоичного кода:

go test -c -coverpkg=.  -o myProgram

Затем мои функциональные тесты запускают этот двоичный файл покрытия, например:

./myProgram -test.coverprofile=/tmp/profile
6507374435908599516
PASS
coverage: 64.3% of statements in .

И я строю вывод HTML, показывающий покрытие:

$ go tool cover -html /tmp/profile -o /tmp/profile.html
$ open /tmp/profile.html

профиль покрытия html

проблема

метод exit никогда не будет показывать 100% покрытие из-за условия if flag.Lookup("test.coverprofile") != nil, Итак, линия os.Exit(code) это своего рода слепое пятно для моих результатов покрытия, хотя на самом деле функциональные тесты идут по этой строке, и эта строка должна быть показана зеленым цветом.

С другой стороны, если я уберу условие if flag.Lookup("test.coverprofile") != nil, линия os.Exit(code) прервет мой бинарный файл без создания профиля покрытия.

Как переписать exit() и возможно main_test.go показать покрытие без слепых зон?

Первое решение, которое приходит на ум, это time.Sleep():

func exit(code int) {
        exitCh <- code
        time.Sleep(time.Second) // wait some time to let coverprofile be written
        os.Exit(code)
    }
}

Но это не очень хорошо, потому что перед выходом будет замедлен рабочий код.

1 ответ

Решение

Согласно нашему разговору в комментариях, наш профиль покрытия никогда не будет включать эту строку кода, потому что он никогда не будет выполнен.

Без просмотра вашего полного кода трудно найти правильные решения, однако есть несколько вещей, которые вы можете сделать, чтобы увеличить охват, не жертвуя слишком большим количеством.

func Main и TestMain

Стандартная практика для GOLANG Избегайте тестирования основной точки входа в приложение, чтобы большинство специалистов извлекали как можно больше функциональности из других классов, чтобы их можно было легко протестировать.

GOLANG Среда тестирования позволяет вам тестировать ваше приложение без основной функции, но в этом месте вы можете использовать функцию TestMain, которую можно использовать для тестирования, где код должен выполняться в главном потоке. Ниже приведен небольшой пример из GOLANG Testing.

Иногда тестовой программе необходимо выполнить дополнительную настройку или демонтаж до или после тестирования. Иногда для проверки необходимо проверить, какой код выполняется на main нить. Для поддержки этих и других случаев, если тестовый файл содержит функцию: func TestMain(m *testing.M)

Проверьте GOLANG Testing для получения дополнительной информации.

Рабочий пример

Ниже приведен пример (с охватом 93,3%, который мы сделаем на 100%), который проверяет все функциональные возможности вашего кода. Я внес несколько изменений в ваш дизайн, потому что он не очень хорошо подходил для тестирования, но функциональность осталась прежней.

основной пакет

dofunc.go

import (
    "fmt"
    "math/rand"
    "time"
)

var seed int64 = time.Now().UTC().UnixNano()

func doFunc() int {
    rand.Seed(seed)
    var code int
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            code = 0
            break
        }
        if i%2 == 0 {
            fmt.Println("status 1")
            code = 1
            break
        }
        time.Sleep(time.Second)
    }
    return code
}

dofunc_test.go

package main

import (
    "testing"
    "flag"
    "os"
)

var exitCode int

func TestMain(m *testing.M) {
    flag.Parse()
    code := m.Run()
    os.Exit(code)
}

func TestDoFuncErrorCodeZero(t *testing.T) {
    seed = 2

    if code:= doFunc(); code != 0 {
        t.Fail()
    }
}

func TestDoFuncErrorCodeOne(t *testing.T) {
    seed = 3

    if code:= doFunc(); code != 1 {
        t.Fail()
    }
}

main.go

package main

import "os"

func main() {
    os.Exit(doFunc());
}

Запуск тестов

Если мы создаем наше приложение с профилем обложки.

$ go test -c -coverpkg=. -o example

И запустить его.

$ ./example -test.coverprofile=/tmp/profile

Запуск тестов

1543039099823358511
2444694468985893231
6640668014774057861
6019456696934794384
status 1
PASS
coverage: 93.3% of statements in .

Итак, мы видим, что мы получили 93% покрытия, которое мы знаем, потому что у нас нет тестового покрытия для main чтобы исправить это, мы могли бы написать несколько тестов для этого (не очень хорошая идея), так как код имеет os.Exit или мы можем реорганизовать его так, чтобы он был очень простым с очень малой функциональностью, и мы можем исключить его из наших тестов.

Чтобы исключить main.go файл из отчетов о покрытии мы можем использовать сборку tags разместив комментарий тега в первой строке main.go файл.

//+build !test

Для получения дополнительной информации о флагах сборки проверьте эту ссылку: http://dave.cheney.net/2013/10/12/how-to-use-conditional-compilation-with-the-go-build-tool

Это скажет GOLANG что файл должен быть включен в процесс сборки, где присутствует тег сборки, но NOT где тест тегов присутствует.

Смотрите полный код.

//+build !test

package main

import "os"

func main() {
    os.Exit(doFunc());
}

Нам нужно построить приложение покрытия немного по-другому.

$ go test -c -coverpkg=. -o example -tags test

Запуск это будет то же самое.

$ ./example -test.coverprofile=/tmp/profile

Мы получаем отчет ниже.

1543039099823358511
2444694468985893231
6640668014774057861
6019456696934794384
status 1
PASS
coverage: 100.0% of statements in .

Теперь мы можем построить покрытие HTML.

$ go tool cover -html /tmp/profile -o /tmp/profile.html

Покрытие HTML Report

В моем проекте pkglint я объявил переменную, видимую для пакета:

var exit = os.Exit

В коде, который настраивает тест, я перезаписываю его специальной функцией теста, а при разрыве теста я сбрасываю его обратно на os.Exit.

Это простое и прагматичное решение, которое хорошо работает для меня, по крайней мере, в течение года тщательного тестирования. Я получаю 100% -ное покрытие веток, потому что в них вообще нет веток.

Другие вопросы по тегам