Как проверить 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 в своих тестах:

Go Playground

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

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