Показывает охват функциональных тестов без слепых зон
У меня есть рабочий код 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
проблема
метод 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
В моем проекте pkglint я объявил переменную, видимую для пакета:
var exit = os.Exit
В коде, который настраивает тест, я перезаписываю его специальной функцией теста, а при разрыве теста я сбрасываю его обратно на os.Exit.
Это простое и прагматичное решение, которое хорошо работает для меня, по крайней мере, в течение года тщательного тестирования. Я получаю 100% -ное покрытие веток, потому что в них вообще нет веток.