Как проверить os.exit сценарии в Go
Учитывая этот код
func doomed() {
os.Exit(1)
}
Как правильно проверить, что вызов этой функции приведет к существованию, используя go test
? Это должно происходить в наборе тестов, другими словами os.Exit()
вызов не может повлиять на другие тесты и должен быть захвачен.
7 ответов
Есть презентация Эндрю Герранда (одного из основных членов команды Go), где он показывает, как это сделать.
Дана функция (в main.go
)
package main
import (
"fmt"
"os"
)
func Crasher() {
fmt.Println("Going down in flames!")
os.Exit(1)
}
вот как бы вы это проверили (через main_test.go
):
package main
import (
"os"
"os/exec"
"testing"
)
func TestCrasher(t *testing.T) {
if os.Getenv("BE_CRASHER") == "1" {
Crasher()
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() {
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}
Что делает код, так это вызывает go test
снова в отдельном процессе через exec.Command
, ограничивая исполнение TestCrasher
тест (через -test.run=TestCrasher
переключатель). Он также передает флаг через переменную окружения (BE_CRASHER=1
) который проверяется вторым вызовом и, если установлен, вызывает тестируемую систему, возвращаясь сразу после этого, чтобы предотвратить запуск в бесконечный цикл. Таким образом, мы возвращаемся на наш исходный сайт вызовов и можем теперь проверить фактический код выхода.
Источник: Слайд 23 презентации Андрея. Второй слайд также содержит ссылку на видео презентации. Он говорит о тестах подпроцесса в 47:09
Я делаю это с помощью бук / обезьяна:
func TestDoomed(t *testing.T) {
fakeExit := func(int) {
panic("os.Exit called")
}
patch := monkey.Patch(os.Exit, fakeExit)
defer patch.Unpatch()
assert.PanicsWithValue(t, "os.Exit called", doomed, "os.Exit was not called")
}
Обезьяна сверхмощна, когда дело доходит до такого рода работы, а также для выявления неисправностей и других сложных задач. Это идет с некоторыми оговорками.
Я не думаю, что вы можете проверить фактическое os.Exit
без симуляции тестирования извне (используя exec.Command
) процесс.
Тем не менее, вы можете достичь своей цели, создав интерфейс или тип функции, а затем использовать реализацию noop в своих тестах:
package main
import "os"
import "fmt"
type exiter func (code int)
func main() {
doExit(func(code int){})
fmt.Println("got here")
doExit(func(code int){ os.Exit(code)})
}
func doExit(exit exiter) {
exit(1)
}
Вы не можете, вам придется использовать exec.Command
и проверить возвращаемое значение.
Код для тестирования:
package main
import "os"
var my_private_exit_function func(code int) = os.Exit
func main() {
MyAbstractFunctionAndExit(1)
}
func MyAbstractFunctionAndExit(exit int) {
my_private_exit_function(exit)
}
Тестовый код:
package main
import (
"os"
"testing"
)
func TestMyAbstractFunctionAndExit(t *testing.T) {
var ok bool = false // The default value can be omitted :)
// Prepare testing
my_private_exit_function = func(c int) {
ok = true
}
// Run function
MyAbstractFunctionAndExit(1)
// Check
if ok == false {
t.Errorf("Error in AbstractFunction()")
}
// Restore if need
my_private_exit_function = os.Exit
}
В моем коде я только что использовал
func doomedOrNot() int {
if (doomed) {
return 1
}
return 0
}
затем назовем это так:
if exitCode := doomedOrNot(); exitCode != 0 {
os.Exit(exitCode)
}
СюдаdoomedOrNot
можно легко протестировать.
Чтобы проверитьos.Exit
как и сценарии, мы можем использовать https://github.com/undefinedlabs/go-mpatch вместе с приведенным ниже кодом. Это гарантирует, что ваш код останется чистым, а также удобочитаемым и ремонтопригодным.
type PatchedOSExit struct {
Called bool
CalledWith int
patchFunc *mpatch.Patch
}
func PatchOSExit(t *testing.T, mockOSExitImpl func(int)) *PatchedOSExit {
patchedExit := &PatchedOSExit{Called: false}
patchFunc, err := mpatch.PatchMethod(os.Exit, func(code int) {
patchedExit.Called = true
patchedExit.CalledWith = code
mockOSExitImpl(code)
})
if err != nil {
t.Errorf("Failed to patch os.Exit due to an error: %v", err)
return nil
}
patchedExit.patchFunc = patchFunc
return patchedExit
}
func (p *PatchedOSExit) Unpatch() {
_ = p.patchFunc.Unpatch()
}
Вы можете использовать приведенный выше код следующим образом:
func NewSampleApplication() {
os.Exit(101)
}
func Test_NewSampleApplication_OSExit(t *testing.T) {
// Prepare mock setup
fakeExit := func(int) {}
p := PatchOSExit(t, fakeExit)
defer p.Unpatch()
// Call the application code
NewSampleApplication()
// Assert that os.Exit gets called
if p.Called == false {
t.Errorf("Expected os.Exit to be called but it was not called")
return
}
// Also, Assert that os.Exit gets called with the correct code
expectedCalledWith := 101
if p.CalledWith != expectedCalledWith {
t.Errorf("Expected os.Exit to be called with %d but it was called with %d", expectedCalledWith, p.CalledWith)
return
}
}
Я также добавил ссылку на игровую площадку: https://go.dev/play/p/FA0dcwVDOm7