Тестирование os.Выходные сценарии в Go с информацией о покрытии (coveralls.io/Goveralls)
Этот вопрос: Как проверить сценарии os.exit в Go (и получивший наивысший ответ там) описывает, как проверить os.Exit()
сценарии в ходу. Как os.Exit()
не может быть легко перехвачен, используемый метод состоит в том, чтобы повторно вызвать двоичный файл и проверить значение выхода. Этот метод описан на слайде 23 этой презентации Эндрю Геррандом (один из основных членов команды Go); код очень прост и полностью воспроизведен ниже.
Соответствующие тестовые и основные файлы выглядят следующим образом (обратите внимание, что только эта пара файлов является MVCE):
package foo
import (
"os"
"os/exec"
"testing"
)
func TestCrasher(t *testing.T) {
if os.Getenv("BE_CRASHER") == "1" {
Crasher() // This causes os.Exit(1) to be called
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
cmd.Env = append(os.Environ(), "BE_CRASHER=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
fmt.Printf("Error is %v\n", e)
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}
а также
package foo
import (
"fmt"
"os"
)
// Coverage testing thinks (incorrectly) that the func below is
// never being called
func Crasher() {
fmt.Println("Going down in flames!")
os.Exit(1)
}
Однако этот метод, по-видимому, имеет определенные ограничения:
Тестирование покрытия с помощью goveralls / coveralls.io не работает - см., Например, пример здесь (тот же код, что и выше, но для удобства добавлен в github), который производит здесь тест покрытия, то есть он не записывает выполняемые функции тестирования. Обратите внимание, что вам не нужны эти ссылки, чтобы ответить на вопрос - приведенный выше пример будет работать нормально - они просто показывают, что произойдет, если вы поместите вышеупомянутое в github, и проведете его через travis до coveralls.io
Перезапуск тестового двоичного файла выглядит хрупким.
В частности, по запросу, здесь приведен скриншот (а не ссылка) для сбоя покрытия; красная заливка указывает на то, что для coveralls.io Crasher()
не вызывается.
Это можно обойти? Особенно первый пункт.
На уровне Голанга проблема заключается в следующем:
Goveralls работает каркас
go test -cover ...
, который вызывает тест выше.Тест выше звонков
exec.Command / .Run
без-cover
в аргументах ОСБезоговорочно выкладываю
-cover
и т. д. в списке аргументов непривлекательно, так как он будет запускать тест покрытия (как подпроцесс) в тесте без покрытия и анализировать список аргументов на наличие-cover
и т. д. кажется сверхмощным решением.Даже если я положу
-cover
и т.д. в списке аргументов, я понимаю, что тогда у меня будет два выходных покрытия, записанных в один и тот же файл, что не сработает - они должны были бы каким-то образом объединиться. Самое близкое, что я имею к этому, это проблема Голанга.
Резюме
Что мне нужно, так это простой способ запустить тестирование покрытия (предпочтительно через travis, goveralls и coveralls.io), где можно выполнить оба тестовых случая, когда тестируемая процедура завершается с OS.exit()
и где отмечен охват этого теста. Я бы очень хотел использовать выше метод re-exec (если это можно сделать, чтобы работать), если это можно сделать, чтобы работать.
Решение должно показать тестирование покрытия Crasher()
, Исключая Crasher()
тестирование покрытия - это не вариант, так как в реальном мире я пытаюсь протестировать более сложную функцию, где где-то глубоко внутри, при определенных условиях, она вызывает: log.Fatalf()
; что я тестирую на предмет покрытия, так это то, что тесты для этих условий работают должным образом.
3 ответа
С небольшим рефакторингом вы можете легко достичь 100% покрытия.
foo/bar.go
:
package foo
import (
"fmt"
"os"
)
var osExit = os.Exit
func Crasher() {
fmt.Println("Going down in flames!")
osExit(1)
}
И код тестирования: foo/bar_test.go
:
package foo
import "testing"
func TestCrasher(t *testing.T) {
// Save current function and restore at the end:
oldOsExit := osExit
defer func() { osExit = oldOsExit }()
var got int
myExit := func(code int) {
got = code
}
osExit = myExit
Crasher()
if exp := 1; got != exp {
t.Errorf("Expected exit code: %d, got: %d", exp, got)
}
}
Бег go test -cover
:
Going down in flames!
PASS
coverage: 100.0% of statements
ok foo 0.002s
Да, вы можете сказать, что это работает, если os.Exit()
вызывается явно, но что если os.Exit()
вызывается кем-то другим, например log.Fatalf()
?
Там тоже работает та же техника, нужно просто переключиться log.Fatalf()
вместо os.Exit()
Например:
Соответствующая часть foo/bar.go
:
var logFatalf = log.Fatalf
func Crasher() {
fmt.Println("Going down in flames!")
logFatalf("Exiting with code: %d", 1)
}
И код тестирования: TestCrasher()
в foo/bar_test.go
:
func TestCrasher(t *testing.T) {
// Save current function and restore at the end:
oldLogFatalf := logFatalf
defer func() { logFatalf = oldLogFatalf }()
var gotFormat string
var gotV []interface{}
myFatalf := func(format string, v ...interface{}) {
gotFormat, gotV = format, v
}
logFatalf = myFatalf
Crasher()
expFormat, expV := "Exiting with code: %d", []interface{}{1}
if gotFormat != expFormat || !reflect.DeepEqual(gotV, expV) {
t.Error("Something went wrong")
}
}
Бег go test -cover
:
Going down in flames!
PASS
coverage: 100.0% of statements
ok foo 0.002s
Интерфейсы и макеты
Использование интерфейсов Go позволяет создавать макетные композиции. Тип может иметь интерфейсы в виде связанных зависимостей. Эти зависимости могут быть легко заменены на макеты, соответствующие интерфейсам.
type Exiter interface {
Exit(int)
}
type osExit struct {}
func (o* osExit) Exit (code int) {
os.Exit(code)
}
type Crasher struct {
Exiter
}
func (c *Crasher) Crash() {
fmt.Println("Going down in flames!")
c.Exit(1)
}
тестирование
type MockOsExit struct {
ExitCode int
}
func (m *MockOsExit) Exit(code int){
m.ExitCode = code
}
func TestCrasher(t *testing.T) {
crasher := &Crasher{&MockOsExit{}}
crasher.Crash() // This causes os.Exit(1) to be called
f := crasher.Exiter.(*MockOsExit)
if f.ExitCode == 1 {
fmt.Printf("Error code is %d\n", f.ExitCode)
return
}
t.Fatalf("Process ran with err code %d, want exit status 1", f.ExitCode)
}
Недостатки
оригинал Exit
Метод по-прежнему не будет тестироваться, поэтому он должен отвечать только за выход, не более того.
Функции граждан первого класса
Зависимость параметров
Функции первоклассных граждан в Go. С функциями разрешено много операций, поэтому мы можем напрямую выполнять некоторые приемы с функциями.
Используя операцию "передать как параметр", мы можем сделать внедрение зависимости:
type osExit func(code int)
func Crasher(os_exit osExit) {
fmt.Println("Going down in flames!")
os_exit(1)
}
Тестирование:
var exit_code int
func os_exit_mock(code int) {
exit_code = code
}
func TestCrasher(t *testing.T) {
Crasher(os_exit_mock) // This causes os.Exit(1) to be called
if exit_code == 1 {
fmt.Printf("Error code is %d\n", exit_code)
return
}
t.Fatalf("Process ran with err code %v, want exit status 1", exit_code)
}
Недостатки
Вы должны передать зависимость в качестве параметра. Если у вас много зависимостей, длина списка параметров может быть огромной.
Подстановка переменных
На самом деле это можно сделать с помощью операции "присвоить переменной" без явной передачи функции в качестве параметра.
var osExit = os.Exit
func Crasher() {
fmt.Println("Going down in flames!")
osExit(1)
}
тестирование
var exit_code int
func osExitMock(code int) {
exit_code = code
}
func TestCrasher(t *testing.T) {
origOsExit := osExit
osExit = osExitMock
// Don't forget to switch functions back!
defer func() { osExit = origOsExit }()
Crasher()
if exit_code != 1 {
t.Fatalf("Process ran with err code %v, want exit status 1", exit_code)
}
}
недостатки
Это неявно и легко потерпеть крах.
Дизайн заметки
Если вы планируете объявить некоторую логику ниже Exit
логика выхода должна быть изолирована else
блок или дополнительный return
после выхода, потому что макет не остановит выполнение.
func (c *Crasher) Crash() {
if SomeCondition == true {
fmt.Println("Going down in flames!")
c.Exit(1) // Exit in real situation, invoke mock when testing
} else {
DoSomeOtherStuff()
}
}
Не принято ставить тесты вокруг Main
функция приложения в GOLANG
особенно из-за таких вопросов. Был вопрос, на который уже дан ответ, который затронул эту же проблему.
Подвести итоги
Подводя итог, вы должны избегать размещения тестов вокруг основной точки входа в приложение и пытаться спроектировать ваше приложение так, чтобы на нем было мало кода. Main
Функция настолько отсоединена, что позволяет вам тестировать как можно больше кода.
Проверьте GOLANG Testing для получения дополнительной информации.
Покрытие до 100%
Как я подробно изложил в предыдущем ответе, так как это плохая идея, чтобы попытаться получить тесты вокруг Main
Функциональность и наилучшая практика - размещать как можно меньше кода, чтобы он мог быть протестирован надлежащим образом, без слепых зон, что само собой разумеется, что попытка получить 100% охват при попытке включить Main
func - это напрасная трата усилий, поэтому лучше игнорировать ее в тестах.
Вы можете использовать теги сборки, чтобы исключить main.go
файл из тестов, следовательно, достигнув 100% покрытия или все зеленого цвета.
Проверка: отображение покрытия функциональных тестов без слепых зон
Если вы хорошо разрабатываете свой код и сохраняете всю действующую функциональность в отлаженном состоянии и тестируете, имея несколько строк кода, которые мало что делают, а затем вызывают фактические фрагменты кода, которые выполняют всю реальную работу и хорошо тестируются, это на самом деле не имеет значения что вы не тестируете крошечный и не значимый код.