Недавно я столкнулся с проблемой при написании юнит-теста логгера, если я выполняю его напрямуюlogger.Fatal
, потому что основная функция этой функции называетсяos.Exit(1)
, то процесс будет прекращен напрямую, а тестирующий пакет будет думать, что тест провален.Хотя это простая функция, и она почти бесполезна, но из-за обсессивно-компульсивного расстройства для него надо устроить юнит-тест.
Позже я случайно нашел слайд о методах тестирования Эндрю Герранда (одного из разработчиков Golang) на Google I/O 2014 (см.приложение), в котором говорится о тестах подпроцессов, то есть тестах подпроцессов, следующим образом:
Sometimes you need to test the behavior of a process, not just a function.
func Crasher() {
fmt.Println("Going down in flames!")
os.Exit(1)
}
To test this code, we invoke the test binary itself as a subprocess:
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)
}
Здесь упоминается, что если мы хотим протестировать поведение процесса, а не только функции, то мы можем создать подпроцесс через двоичный файл модульного теста для тестирования. Итак, возвращаясь назад, с точки зрения тестирования, нам нужно протестироватьFatal
Эти два поведения функции:
- распечатать текст журнала
- Завершить процесс с ошибкой
Таким образом, наш модульный тест должен охватывать эти два поведения функции, и навыки тестирования подпроцессов, упомянутые Эндрю Джеррандом, подходят для этой ситуации. Таким образом, вы можете обратиться к этому примеру, даваяFatal
написать модульный тест
Предположим, что наша фатальная функция представляет собой пакет, основанный на стандартном библиотечном пакете журналов.
package logger
func Fatal(v ...interface{}){
log.Fatal(v...)
}
Здесь мы можем сначала взглянуть на реализацию стандартного библиотечного лог-пакета (на самом деле zap/logrus и другие лог-пакетыFatal
Функция также похожа и, наконец, вызываетos.Exit(1)
)
func Fatal(v ...interface{}) {
std.Output(2, fmt.Sprint(v...)) //输出到标准输出/标准错误输出
os.Exit(1) // 有错误地退出进程
}
Сначала напишите модульный тест обычным способом.
func TestFatal(t *testing.T) {
Fatal("fatal log")
}
Результат выполнения юнит-теста следующий, как я уже говорил, результат FAIL
go test -v
=== RUN TestFatal
2020/01/11 11:39:24 fatal log
exit status 1
FAIL github.com/YouEclipse/mytest/log 0.001
Давайте попробуем написать тест подпроцесса, здесь я распечатываю стандартный вывод и стандартный вывод ошибок
func TestFatal(t *testing.T) {
if os.Getenv("SUB_PROCESS") == "1" {
Fatal("fatal log")
return
}
var outb, errb bytes.Buffer
cmd := exec.Command(os.Args[0], "-test.run=TestFatal")
cmd.Env = append(os.Environ(), "SUB_PROCESS=1")
cmd.Stdout = &outb
cmd.Stderr = &errb
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
fmt.Print(cmd.Stderr)
fmt.Print(cmd.Stdout)
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}
Выполните модульный тест, и результат будет успешным, оправдав наши ожидания.
go test -v
=== RUN TestFatal
2020/01/11 11:40:38 fatal log
--- PASS: TestFatal (0.00s)
PASS
ok github.com/YouEclipse/mytest/log 0.002s
Конечно, мы должны не только знать, что есть, но и знать, почему. Разберем, почему тестовый код подпроцесса написан именно так
-
пройти через
os.Getenv
Получите переменную среды, значение здесь пустое, поэтому Fatal не будет выполняться -
Определенный
outb
,errb
, здесь для последующего захвата stdout и stderr -
перечислить
exec.Command
Построить структуру Cmd на основе переданных параметров
exec — это пакет в стандартной библиотеке, предназначенный для выполнения команд, поэтому я не буду вдаваться в подробности.
Мы видим, что первый параметр exec.Cmmand — это имя команды или двоичного файла, который нужно выполнить, а второй параметр — неопределенный параметр, который является параметром команды, которую нам нужно выполнить.
Здесь мы передаем первый параметрos.Args[0]
, os.Args[0]
— это путь к бинарному файлу программы при запуске программы, а второй параметр — это параметр при выполнении бинарного файла. Что касается того, почемуos.Args[0]
вместоos.Args[1]
илиos.Args[2]
Ну давайте выполнимgo test -n
, вы увидите множество выходных данных (самое нерелевантное опущено)
mkdir -p $WORK/b001/
#
# internal/cpu
#
...
/usr/local/go/pkg/tool/linux_amd64/compile -o ./_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid 5WmoKx2_LnkcztVfW1Bj/5WmoKx2_LnkcztVfW1Bj -dwarf=false -goversion go1.13.5 -D "" -importcfg ./importcfg -pack -c=4 ./_testmain.go
...
/usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/log.test -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=o8I_q2gkkk-Xda8yeh2G/5WmoKx2_LnkcztVfW1Bj/5WmoKx2_LnkcztVfW1Bj/o8I_q2gkkk-Xda8yeh2G -extld=gcc $WORK/b001/_pkg_.a
...
cd /home/yoyo/go/src/github.com/YouEclipse/mytest/log
TERM='dumb' /usr/local/go/pkg/tool/linux_amd64/vet -atomic -bool -buildtags -errorsas -nilfunc -printf $WORK/b052/vet.cfg
$WORK/b001/log.test -test.timeout=10m0s
Из вывода мы можем знатьgo test
Последним шагом является компиляция и компоновка исходных файлов в двоичные файлы (и, конечно, статические проверки govet) для выполнения. Фактическиgo test
иgo run
Наконец вызывается та же самая функция, и в этой статье она не слишком подробно обсуждается, подробности можно посмотреть в исходном коде.cmd/go/internal/test/test.go
иcmd/go/internal/work/build.go
содержимое этих двух файлов.
и-n
параметры, которые можно распечататьgo test
илиgo run
Все команды используются в процессе выполнения, поэтому мы выполняем окончательный бинарный файл и выводим его на последнюю строку вывода.-test.timeout=10m0s
Флаг тайм-аута по умолчанию. А os.Args — это константа пакета os, при запуске процесса в него будет записана исполняемая команда и флаг
// Args hold the command-line arguments, starting with the program name.
var Args []string
такos.Args[0]
Естественно, получается полное имя файла скомпилированного бинарника.
второй параметр-test.run=TestFatal
флаг для выполнения бинарного файла,test.run
Имя функции теста, заданное флагом.
когда мы выполняемgo test -run TestFatal
, на самом деле окончательное исполнение$WORK/b001/log.test -run=TestFatal
Другие флаги могут быть выполненыgo help testflag
Посмотреть или обратитьсяcmd/go/internal/test/testflag.go
в файлеtestFlagDefn
Входящие, конкретные определения и описания находятся в исходном коде.
- cmd.Env устанавливает переменную среды для запуска дочернего процесса.
os.Environ() получает копию текущей переменной среды, мы добавляем
SUB_PROCESS
Пользователь переменной среды определяет, является ли процесс дочерним. - cmd.Stdout = &outb
- cmd.Stderr = &ошибка Захватите вывод аннотации и стандартную ошибку во время работы подпроцесса, потому что нам нужно проверить, соответствует ли вывод
- cmd.Выполнить()
Запустите дочерний процесс и дождитесь возврата результата. Может вернуться, если он завершится.
exec.ExitError
, вы можете получить exited statusCode, и наша цель — проверить, завершается ли процесс - В дочернем процессе переменная окружения в это время
SUB_PROCESS
значение1
, то он будет выполнятьсяFatal
Функция, основной процесс получает код выхода и печатает вывод дочернего процесса.
Пока что принцип этого тестового кода ясен.
Однако ложка дегтя заключается в том, что в реализацииgo test -cover
При выполнении статистики тестового покрытия функции модульных тестов, запущенных подпроцессами, учитываться не будут.
приложение
- Testing Techniques - Andrew Gerrand