Как проверить функцию Go, содержащую log.Fatal()

Скажем, у меня был следующий код, который печатает некоторые сообщения журнала. Как я могу проверить, что правильные сообщения были зарегистрированы? Поскольку log.Fatal вызывает os.Exit(1), тесты терпят неудачу.

package main

import (
    "log"
)

func hello() {
    log.Print("Hello!")
}

func goodbye() {
    log.Fatal("Goodbye!")
}

func init() {
    log.SetFlags(0)
}

func main() {
    hello()
    goodbye()
}

Вот гипотетические тесты:

package main

import (
    "bytes"
    "log"
    "testing"
)


func TestHello(t *testing.T) {
    var buf bytes.Buffer
    log.SetOutput(&buf)

    hello()

    wantMsg := "Hello!\n"
    msg := buf.String()
    if msg != wantMsg {
        t.Errorf("%#v, wanted %#v", msg, wantMsg)
    }
}

func TestGoodby(t *testing.T) {
    var buf bytes.Buffer
    log.SetOutput(&buf)

    goodbye()

    wantMsg := "Goodbye!\n"
    msg := buf.String()
    if msg != wantMsg {
        t.Errorf("%#v, wanted %#v", msg, wantMsg)
    }
}

Ответ 1

Это похоже на " Как протестировать os.Exit() в Go ": вам нужно реализовать свой собственный регистратор, который по умолчанию перенаправляет на log.xxx(), но дает вам возможность при тестировании заменить функцию как log.Fatalf() с вашим собственным (который не вызывает os.Exit(1))

Я сделал то же самое для тестирования os.Exit() в exit/exit.go:

exiter = New(func(int) {})
exiter.Exit(3)
So(exiter.Status(), ShouldEqual, 3)

(здесь моя функция выхода - пустая, которая ничего не делает)

Ответ 2

Я использовал следующий код для проверки моей функции. В xxx.go:

var logFatalf = log.Fatalf

if err != nil {
    logFatalf("failed to init launcher, err:%v", err)
}

И в xxx_test.go:

// TestFatal is used to do tests which are supposed to be fatal
func TestFatal(t *testing.T) {
    origLogFatalf := logFatalf

    // After this test, replace the original fatal function
    defer func() { logFatalf = origLogFatalf } ()

    errors := []string{}
    logFatalf = func(format string, args ...interface{}) {
        if len(args) > 0 {
            errors = append(errors, fmt.Sprintf(format, args))
        } else {
            errors = append(errors, format)
        }
    }
    if len(errors) != 1 {
        t.Errorf("excepted one error, actual %v", len(errors))
    }
}

Ответ 3

Пока можно проверить код, содержащий log.Fatal, не рекомендуется. В частности, вы не можете проверить этот код таким образом, который поддерживается флагом -cover на go test.

Вместо этого рекомендуется изменить код, чтобы вернуть ошибку вместо вызова журнала. Fatal. В последовательной функции вы можете добавить дополнительное возвращаемое значение, а в goroutine вы можете передать ошибку на канал типа chan error (или некоторый тип структуры, содержащий поле типа ошибки).

После того, как это изменение будет сделано, ваш код будет намного легче читать, гораздо легче протестировать, и он будет более переносимым (теперь вы можете использовать его в серверной программе в дополнение к инструментам командной строки).

Если у вас есть вызовы log.Println, я также рекомендую передать пользовательский регистратор в качестве поля на ресивере. Таким образом, вы можете войти в пользовательский регистратор, который вы можете установить на stderr или stdout для сервера, и noop logger для тестов (чтобы вы не получали кучу ненужного вывода в своих тестах). Пакет log поддерживает пользовательские журналы, поэтому нет необходимости писать свой собственный или импортировать для него сторонний пакет.

Ответ 4

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

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

Чтобы ответить на вопрос о том, что правильные сообщения были зарегистрированы, вы должны проверить внутренний процесс cmd.Stdout.

https://play.golang.org/p/J8aiO9_NoYS

func TestFooFatals(t *testing.T) {
    fmt.Println("TestFooFatals")
    outer := os.Getenv("FATAL_TESTING") == ""
    if outer {
        fmt.Println("Outer process: Spawning inner 'go test' process, looking for failure from fatal")
        cmd := exec.Command(os.Args[0], "-test.run=TestFooFatals")
        cmd.Env = append(os.Environ(), "FATAL_TESTING=1")
        // cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
        err := cmd.Run()
        fmt.Printf("Outer process: Inner process returned %v\n", err)
        if e, ok := err.(*exec.ExitError); ok && !e.Success() {
            // fmt.Println("Success: inner process returned 1, passing test")
            return
        }
        t.Fatalf("Failure: inner function returned %v, want exit status 1", err)
    } else {
        // We're in the spawned process.
        // Do something that should fatal so this test fails.
        foo()
    }
}

// should fatal every time
func foo() {
    log.Printf("oh my goodness, i see %q\n", os.Getenv("FATAL_TESTING"))
    // log.Fatal("oh my gosh")
}

Ответ 5

Если вы используете logrus, теперь есть возможность определить функцию выхода из v1.3.0, введенную в этом коммите. Таким образом, ваш тест может выглядеть примерно так:

func Test_X(t *testing.T) {
    cases := []struct{
        param string
        expectFatal bool
    }{
        {
            param: "valid",
            expectFatal: false,
        },
        {
            param: "invalid",
            expectFatal: true,
        },
    }

    defer func() { log.StandardLogger().ExitFunc = nil }()
    var fatal bool
    log.StandardLogger().ExitFunc = func(int){ fatal = true }

    for _, c := range cases {
        fatal = false
        X(c.param)
        assert.Equal(t, c.expectFatal, fatal)
    }
}

Ответ 6

Я бы использовал чрезвычайно удобный bouk/monkey пакет (здесь наряду с stretchr/testify).

func TestGoodby(t *testing.T) {
  wantMsg := "Goodbye!"

  fakeLogFatal := func(msg ...interface{}) {
    assert.Equal(t, wantMsg, msg[0])
    panic("log.Fatal called")
  }
  patch := monkey.Patch(log.Fatal, fakeLogFatal)
  defer patch.Unpatch()
  assert.PanicsWithValue(t, "log.Fatal called", goodbye, "log.Fatal was not called")
}

Советую прочитать предостережения от использования bouk/monkey перед тем, как идти по этому маршруту.

Ответ 7

Вы не можете и не должны. Это "вы должны" протестировать "каждую строку" - отношение странно, особенно для терминальных условий и того, для чего нужен log.Fatal. (Или просто проверить его снаружи.)