Тестирование
Для разработчика хорошее тестирование позволяет заранее обнаружить ошибки в программе, избежать умственной нагрузки из-за возникновения багов в результате несвоевременного обслуживания, поэтому очень важно правильно писать тесты. Go предоставляет очень удобный и практичный инструмент командной строки go test для тестирования. Тестирование можно увидеть в стандартной библиотеке и во многих открытых фреймворках. Этот инструмент очень прост в использовании и в настоящее время поддерживает следующие виды тестирования:
- Примеры тестов
- Модульные тесты
- Бенчмарки (тесты производительности)
- Нечёткое тестирование (Fuzzing)
Большинство API для тестирования в Go предоставляются стандартным пакетом testing.
TIP
Выполните команду go help testfunc в командной строке, чтобы увидеть объяснение четырёх вышеупомянутых типов тестов от официальной документации Go.
Правила написания
Прежде чем приступить к написанию тестов, следует отметить несколько правил, которые облегчат дальнейшее обучение.
- Пакет тестов: тестовые файлы лучше всего размещать в отдельном пакете, обычно называемом
test. - Тестовые файлы: тестовые файлы обычно заканчиваются на
_test.go. Например, если вы хотите протестировать определённую функцию, назовите файлfunction_test.go. Если вы хотите разделить тесты по типам, можно использовать тип теста как префикс файла, напримерbenchmark_marshaling_test.goилиexample_marshaling_test.go. - Тестовые функции: каждый тестовый файл содержит несколько тестовых функций для различных тестов. Для разных типов тестов стили именования тестовых функций различаются. Например, для примеров тестов —
ExampleXXXX, для модульных тестов —TestXXXX, для бенчмарков —BenchmarkXXXX, для нечёткого тестирования —FuzzXXXX. Таким образом, даже без комментариев можно понять, какой это тип теста.
TIP
Когда пакет называется testdata, он обычно предназначен для хранения вспомогательных данных для тестов. При выполнении тестов Go игнорирует пакеты с именем testdata.
Следуя вышеупомянутым правилам и развивая хороший стиль тестирования, вы сэкономите много хлопот при будущем обслуживании.
Выполнение тестов
Для выполнения тестов используется команда go test. Рассмотрим пример с реальным кодом. Есть файл для тестирования /say/hello.go:
package say
import "fmt"
func Hello() {
fmt.Println("hello")
}
func GoodBye() {
fmt.Println("bye")
}И тестовый файл /test/example_test.go:
package test
import (
"golearn/say"
)
func ExampleHello() {
say.Hello()
// Output:
// hello
}
func ExampleGoodBye() {
say.GoodBye()
// Output:
// bye
}
func ExampleSay() {
say.Hello()
say.GoodBye()
// Output:
// hello
// bye
}Выполнить эти тесты можно несколькими способами. Например, чтобы выполнить все тестовые случаи в пакете test, можно выполнить следующую команду в директории test:
$ go test ./
PASS
ok golearn/test 0.422s./ означает текущую директорию. Go перекомпилирует все тестовые файлы в директории test, а затем выполнит все тестовые случаи. Из результата видно, что все тесты прошли успешно. После этого можно указать несколько директорий в качестве параметров. Например, следующая команда, очевидно, не найдёт тестовых файлов в главной директории проекта:
$ go test ./ ../
ok golearn/test
? golearn [no test files]TIP
Когда параметров несколько пакетов, Go не выполняет повторно успешно пройденные тесты. При выполнении в конце строки добавляется (cached), чтобы указать, что результат взят из кэша предыдущего выполнения. Go кэширует результаты тестов, когда флаги тестирования находятся в следующем наборе, иначе не кэширует:
-benchtime, -cpu,-list, -parallel, -run, -short, -timeout, -failfast, -vЕсли вы хотите отключить кэширование, добавьте параметр -count=1.
Конечно, можно указать отдельный тестовый файл для выполнения:
$ go test example_test.go
ok command-line-arguments 0.457sИли можно указать отдельный тестовый случай в тестовом файле. Например:
$ go test -run ExampleSay
PASS
ok golearn/test 0.038sВсе три вышеупомянутых случая завершили тестирование, но вывод слишком краток. В этом случае можно добавить параметр -v, чтобы сделать вывод более подробным. Например:
$ go test ./ -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok golearn/test 0.040sТеперь чётко видно порядок выполнения каждого тестового случая, время выполнения, результат выполнения и общее время.
TIP
Команда go test по умолчанию выполняет все модульные тесты, примеры тестов и нечёткое тестирование. Если добавить параметр -bench, будут выполнены все типы тестов. Например:
$ go test -bench .Поэтому нужно использовать параметр -run для указания. Например, команда для выполнения только всех бенчмарков:
$ go test -bench . -run ^$Часто используемые параметры
Тестирование Go имеет множество флагов. Ниже приведены только часто используемые параметры. Для получения более подробной информации рекомендуется использовать команду go help testflag.
| Параметр | Описание |
|---|---|
-o file | Указать имя бинарного файла после компиляции |
-c | Только компилировать тестовые файлы, но не запускать |
-json | Вывод логов тестирования в формате JSON |
-exec xprog | Запуск тестов с помощью xprog, эквивалентно go run |
-bench regexp | Выбрать бенчмарки, соответствующие regexp |
-fuzz regexp | Выбрать нечёткое тестирование, соответствующее regexp |
-fuzztime t | Время автоматического завершения нечёткого тестирования, t — интервал времени; когда единица x, означает количество раз, например 200x |
-fuzzminimizetime t | Минимальное время работы нечёткого тестирования, правила аналогичны выше |
-count n | Запустить тесты n раз, по умолчанию 1 раз |
-cover | Включить анализ покрытия тестов |
-covermode set,count,atomic | Установить режим анализа покрытия |
-cpu | Установить GOMAXPROCS для выполнения тестов |
-failfast | После первого неудачного теста не начинать новые тесты |
-list regexp | Список тестовых случаев, соответствующих regexp |
-parallel n | Разрешить параллельное выполнение тестов, вызвавших t.Parallel, n — максимальное количество параллельных выполнений |
-run regexp | Запускать только тестовые случаи, соответствующие regexp |
-skip regexp | Пропускать тестовые случаи, соответствующие regexp |
-timeout d | Если время выполнения теста превышает интервал d, произойдёт panic. d — интервал времени, например 1s, 1ms, 1ns и т.д. |
-shuffle off,on,N | Перемешать порядок выполнения тестов, N — случайное зерно, по умолчанию зерно — системное время |
-v | Вывод более подробных логов тестирования |
-benchmem | Статистика распределения памяти для бенчмарков |
-blockprofile block.out | Статистика блокировок горутин в тестах и запись в файл |
-blockprofilerate n | Контроль частоты статистики блокировок горутин, см. go doc runtime.SetBlockProfileRate для деталей |
-coverprofile cover.out | Статистика покрытия тестов и запись в файл |
-cpuprofile cpu.out | Статистика CPU и запись в файл |
-memprofile mem.out | Статистика распределения памяти и запись в файл |
-memprofilerate n | Контроль частоты статистики распределения памяти, см. go doc runtime.MemProfileRate для деталей |
-mutexprofile mutex.out | Статистика конкуренции мьютексов и запись в файл |
-mutexprofilefraction n | Установить статистику n горутин, конкурирующих за один мьютекс |
-trace trace.out | Запись отслеживания выполнения в файл |
-outputdir directory | Указать директорию вывода для вышеупомянутых файлов статистики, по умолчанию — директория выполнения go test |
Примеры тестов
Примеры тестов, в отличие от трёх других типов тестов, предназначены не для обнаружения проблем в программе, а скорее для демонстрации использования определённой функции, выполняя роль документации. Примеры тестов — это не официальное определение и не жёсткая спецификация, а скорее инженерное соглашение. Соблюдать его или нет — зависит от разработчика. Примеры тестов очень часто встречаются в стандартной библиотеке, обычно это примеры кода, написанные официальной командой. Например, функция теста ExampleWithDeadline в стандартной библиотеке context/example_test.go демонстрирует базовое использование DeadlineContext:
// This example passes a context with an arbitrary deadline to tell a blocking
// function that it should abandon its work as soon as it gets to it.
func ExampleWithDeadline() {
d := time.Now().Add(shortDuration)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancellation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
// Output:
// context deadline exceeded
}На первый взгляд эта тестовая функция выглядит как обычная функция, но примеры тестов в основном определяются комментарием Output. Когда тестируемая функция имеет только одну строку вывода, используется комментарий Output для проверки вывода. Сначала создадим файл с именем hello.go и запишем следующий код:
package say
import "fmt"
func Hello() {
fmt.Println("hello")
}
func GoodBye() {
fmt.Println("bye")
}Функция SayHello — это тестируемая функция. Затем создадим тестовый файл example_test.go и запишем следующий код:
package test
import (
"golearn/say"
)
func ExampleHello() {
say.Hello()
// Output:
// hello
}
func ExampleGoodBye() {
say.GoodBye()
// Output:
// bye
}
func ExampleSay() {
say.Hello()
say.GoodBye()
// Output:
// hello
// bye
}Комментарий Output в функции указывает, что вывод функции должен быть hello. Далее выполним команду тестирования и посмотрим результат:
$ go test -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok golearn/test 0.448sИз результата видно, что все тесты прошли успешно. Что касается Output, есть несколько способов записи. Первый — с одной строкой вывода, что означает проверку вывода функции на соответствие hello:
// Output:
// helloВторой — с несколькими строками вывода, то есть последовательная проверка вывода на соответствие:
// Output:
// hello
// byeТретий — с неупорядоченным выводом, то есть проверка нескольких строк вывода без учёта порядка:
// Unordered output:
// bye
// helloСледует отметить, что для тестовой функции только последние несколько строк с комментарием Output считаются примером теста. В противном случае это просто обычная функция, которая не будет выполнена Go.
Модульные тесты
Модульное тестирование — это тестирование минимальных тестируемых единиц программного обеспечения. Размер единицы определяется разработчиком: это может быть структура, пакет, функция или тип. Ниже снова продемонстрируем на примере. Сначала создадим файл /tool/math.go и запишем следующий код:
package tool
type Number interface {
~int8 | ~int16 | ~int32 | ~int64 | ~int
}
func SumInt[T Number](a, b T) T {
return a + b
}
func Equal[T Number](a, b T) bool {
return a == b
}Затем создадим тестовый файл /tool_test/unit_test.go. Для модульных тестов имя файла может быть unit_test или с префиксом пакета или функции, которую нужно протестировать:
package test_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Errorf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}Для модульных тестов стиль именования каждого тестового случая — TestXXXX, а параметр функции должен быть t *testing.T. testing.T — это структура, предоставляемая пакетом testing для удобства тестирования, которая предоставляет множество доступных методов. t.Errorf в примере эквивалентен t.Logf и используется для форматированного вывода логов неудачного тестирования. Другие часто используемые методы включают t.Fail для пометки текущего случая как неудачного. Аналогичный функционал имеет t.FailNow, который также помечает тест как неудачный, но первый продолжает выполнение после неудачи, а второй немедленно останавливает выполнение. В следующем примере изменим ожидаемый результат на неправильный:
package tool_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 110
actual := tool.SumInt(a, b)
if actual != expected {
// Errorf использует t.Fail() внутри
t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
t.Log("test finished")
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := true
actual := tool.Equal(a, b)
if actual != expected {
// Fatalf использует t.FailNow() внутри
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
t.Log("test finished")
}Выполним вышеупомянутые тесты, вывод будет следующим:
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
tool_test.go:16: test finished
--- FAIL: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL command-line-arguments 0.037sИз логов тестирования видно, что случай TestSum, несмотря на неудачу, всё равно вывел test finished, а TestEqual — нет. Аналогично t.SkipNow помечает текущий случай как SKIP, затем останавливает выполнение, но в следующем раунде тестирования он будет выполнен снова.
package tool_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 110
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
t.Log("test finished")
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := true
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
t.Log("test finished")
}При выполнении тестов изменим количество запусков на 2:
$ go test tool_test.go -v -count=2
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL command-line-arguments 0.468sВ приведённых выше примерах в последней строке выводилось test finished для обозначения завершения теста. На самом деле можно использовать t.Cleanup для регистрации завершающей функции, которая будет выполнена в конце тестового случая:
package tool_test
import (
"golearn/tool"
"testing"
)
func finished(t *testing.T) {
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Cleanup(func() {
finished(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Cleanup(func() {
finished(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}После выполнения тестов вывод будет следующим:
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:9: test finished
--- PASS: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:9: test finished
--- PASS: TestEqual (0.00s)
PASS
ok command-line-arguments 0.462sHelper
С помощью t.Helper() можно пометить текущую функцию как вспомогательную. Вспомогательная функция не выполняется как отдельный тестовый случай. При записи логов выводится номер строки вызывающего вспомогательную функцию, что делает анализ логов более точным и избегает лишней информации. Например, пример с t.Cleanup выше можно изменить на вспомогательную функцию:
package tool_test
import (
"golearn/tool"
"testing"
)
func CleanupHelper(t *testing.T) {
t.Helper()
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
t.Helper()
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}После выполнения тестов вывод будет следующим. Отличие от предыдущего в том, что номер строки test finished стал номером строки вызывающего:
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:15: test finished
--- PASS: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:30: test finished
--- PASS: TestEqual (0.00s)
PASS
ok command-line-arguments 0.464sTIP
Все вышеупомянутые операции можно выполнять только в основном тесте, то есть в непосредственно выполняемом тестовом случае. Использование в подтесте вызовет panic.
Подтесты
В некоторых случаях может потребоваться тестирование других тестовых случаев внутри одного тестового случая. Такие вложенные тестовые случаи обычно называются подтестами. Метод t.Run() имеет следующую сигнатуру:
// Метод Run запускает новую горутину для выполнения подтеста, блокирует ожидание завершения функции f, затем возвращает
// Возвращаемое значение — успешность теста
func (t *T) Run(name string, f func(t *T)) boolВот пример:
func TestTool(t *testing.T) {
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
}Результат выполнения:
$ go test -run TestTool -v
=== RUN TestTool
=== RUN TestTool/tool.Sum(10,101)
tool_test.go:15: test finished
=== RUN TestTool/tool.Equal(10,101)
tool_test.go:30: test finished
--- PASS: TestTool (0.00s)
--- PASS: TestTool/tool.Sum(10,101) (0.00s)
--- PASS: TestTool/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.449sИз вывода чётко видна иерархическая структура родителей и детей. В приведённом примере второй подтест не выполняется до завершения первого. Можно использовать t.Parallel() для пометки тестовых случаев как параллельно выполняемых. В этом случае порядок вывода будет неопределённым:
package tool_test
import (
"golearn/tool"
"testing"
)
func CleanupHelper(t *testing.T) {
t.Helper()
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Parallel()
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Parallel()
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}
func TestToolParallel(t *testing.T) {
t.Log("setup")
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
t.Log("teardown")
}После выполнения тестов вывод будет следующим:
$ go test -run TestTool -v
=== RUN TestToolParallel
tool_test.go:46: setup
=== RUN TestToolParallel/tool.Sum(10,101)
=== PAUSE TestToolParallel/tool.Sum(10,101)
=== RUN TestToolParallel/tool.Equal(10,101)
=== PAUSE TestToolParallel/tool.Equal(10,101)
=== NAME TestToolParallel
tool_test.go:49: teardown
=== CONT TestToolParallel/tool.Sum(10,101)
=== CONT TestToolParallel/tool.Equal(10,101)
=== NAME TestToolParallel/tool.Sum(10,101)
tool_test.go:16: test finished
=== NAME TestToolParallel/tool.Equal(10,101)
tool_test.go:32: test finished
--- PASS: TestToolParallel (0.00s)
--- PASS: TestToolParallel/tool.Sum(10,101) (0.00s)
--- PASS: TestToolParallel/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.444sИз результатов тестов чётко видно, что есть процесс блокировки ожидания. При параллельном выполнении тестовых случаев, как в приведённом примере, нормальное выполнение невозможно, поскольку нельзя гарантировать синхронное выполнение последующего кода. В этом случае можно вложить ещё один уровень t.Run():
func TestToolParallel(t *testing.T) {
t.Log("setup")
t.Run("process", func(t *testing.T) {
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
})
t.Log("teardown")
}При повторном выполнении можно увидеть нормальный результат:
$ go test -run TestTool -v
=== RUN TestToolParallel
tool_test.go:46: setup
=== RUN TestToolParallel/process
=== RUN TestToolParallel/process/tool.Sum(10,101)
=== PAUSE TestToolParallel/process/tool.Sum(10,101)
=== RUN TestToolParallel/process/tool.Equal(10,101)
=== PAUSE TestToolParallel/process/tool.Equal(10,101)
=== CONT TestToolParallel/process/tool.Sum(10,101)
=== CONT TestToolParallel/process/tool.Equal(10,101)
=== NAME TestToolParallel/process/tool.Sum(10,101)
tool_test.go:16: test finished
=== NAME TestToolParallel/process/tool.Equal(10,101)
tool_test.go:32: test finished
=== NAME TestToolParallel
tool_test.go:51: teardown
--- PASS: TestToolParallel (0.00s)
--- PASS: TestToolParallel/process (0.00s)
--- PASS: TestToolParallel/process/tool.Sum(10,101) (0.00s)
--- PASS: TestToolParallel/process/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.450sТабличный стиль
В приведённых выше модульных тестах входные данные тестов объявлялись как отдельные переменные. Когда данных мало, это не проблема, но если нужно протестировать несколько наборов данных, объявление переменных для создания тестовых данных становится неудобным. Поэтому обычно используется срез структур. Структуры объявляются как временные анонимные структуры. Поскольку такой стиль кодирования выглядит как таблица, он называется table-driven. Ниже приведён пример. Это пример ручного объявления нескольких переменных для создания тестовых данных. Если данных много, это не очень наглядно, поэтому изменим его на табличный стиль:
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}Изменённый код:
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
// table driven style
testData := []struct {
a, b int
exp bool
}{
{10, 101, false},
{5, 5, true},
{30, 32, false},
{100, 101, false},
{2, 3, false},
{4, 4, true},
}
for _, data := range testData {
if actual := tool.Equal(data.a, data.b); actual != data.exp {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", data.a, data.b, data.exp, actual)
}
}
}Такие тестовые данные выглядят гораздо нагляднее.
Бенчмарки
Бенчмарки, также называемые тестами производительности, обычно используются для тестирования таких показателей производительности программы, как использование памяти, использование CPU, время выполнения и т.д. Для бенчмарков тестовые файлы обычно заканчиваются на bench_test.go, а функции тестовых случаев должны быть в формате BenchmarkXXXX.
Ниже приведён пример бенчмарка со сравнением производительности конкатенации строк. Сначала создадим файл /tool/strConcat.go. Как известно, прямая конкатенация строк с помощью + имеет низкую производительность, а использование strings.Builder гораздо лучше. В файле /tool/strings.go создадим две функции для двух способов конкатенации строк:
package tool
import "strings"
func ConcatStringDirect(longString string) {
res := ""
for i := 0; i < 100_000.; i++ {
res += longString
}
}
func ConcatStringWithBuilder(longString string) {
var res strings.Builder
for i := 0; i < 100_000.; i++ {
res.WriteString(longString)
}
}Затем создадим тестовый файл /tool_test/bench_tool_test.go:
package tool_test
import (
"golearn/tool"
"testing"
)
var longString = "longStringlongStringlongStringlongStringlongStringlongStringlongStringlongString"
func BenchmarkConcatDirect(b *testing.B) {
for i := 0; i < b.N; i++ {
tool.ConcatStringDirect(longString)
}
}
func BenchmarkConcatBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
tool.ConcatStringWithBuilder(longString)
}
}Выполним команду тестирования, включив подробные логи и анализ памяти, указав список используемых ядер CPU и выполнив каждый тестовый случай дважды:
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=2
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 277771375 ns/op 4040056736 B/op 10000 allocs/op
BenchmarkConcatDirect-2 4 278500125 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-4 1 1153796000 ns/op 4040068784 B/op 10126 allocs/op
BenchmarkConcatDirect-4 1 1211017600 ns/op 4040073104 B/op 10171 allocs/op
BenchmarkConcatDirect-8 2 665460800 ns/op 4040077760 B/op 10219 allocs/op
BenchmarkConcatDirect-8 2 679774450 ns/op 4040080064 B/op 10243 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 3428 344530 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3579 351858 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 2448 736177 ns/op 4128185 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1688 662993 ns/op 4128185 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1958 550333 ns/op 4128199 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2174 552113 ns/op 4128196 B/op 29 allocs/op
PASS
ok golearn/tool_test 21.381sНиже объясним вывод результатов бенчмарков. goos представляет операционную систему, goarch — архитектуру CPU, pkg — пакет, в котором находится тест, cpu — информация о CPU. Результаты каждого тестового случая разделены именем бенчмарка. Первая колонка BenchmarkConcatDirect-2: цифра 2 представляет количество используемых ядер CPU. Вторая колонка 4 представляет значение b.N в коде, то есть количество циклов в бенчмарке. Третья колонка 277771375 ns/op представляет время, затраченное на каждый цикл (ns — наносекунды). Четвёртая колонка 4040056736 B/op представляет размер выделенной памяти в байтах на каждый цикл. Пятая колонка 10000 allocs/op представляет количество выделений памяти на каждый цикл.
Очевидно, что согласно результатам тестов, производительность strings.Builder значительно выше, чем конкатенация строк с помощью +. Интуитивное сравнение производительности на основе данных — это цель бенчмарков.
benchstat
benchstat — это инструмент анализа производительности с открытым исходным кодом. В приведённых выше тестах производительности было только два образца. Как только образцов становится много, ручной анализ становится очень трудоёмким. Этот инструмент создан для решения проблем анализа производительности.
Сначала нужно загрузить этот инструмент:
$ go install golang.org/x/perf/benchstatВыполним бенчмарки дважды, на этот раз изменив количество образцов на 5, и выведем результаты в файлы old.txt и new.txt для сравнения. Результат первого выполнения:
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a old.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 290535650 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 298974625 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 299637800 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 276487000 ns/op 4040056784 B/op 10001 allocs/op
BenchmarkConcatDirect-2 4 356465275 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-4 2 894723200 ns/op 4040077424 B/op 10216 allocs/op
BenchmarkConcatDirect-4 2 785830400 ns/op 4040078288 B/op 10225 allocs/op
BenchmarkConcatDirect-4 2 743634000 ns/op 4040077568 B/op 10217 allocs/op
BenchmarkConcatDirect-4 2 953802700 ns/op 4040075408 B/op 10195 allocs/op
BenchmarkConcatDirect-4 2 953028750 ns/op 4040077520 B/op 10217 allocs/op
BenchmarkConcatDirect-8 2 684023150 ns/op 4040086784 B/op 10313 allocs/op
BenchmarkConcatDirect-8 2 634380250 ns/op 4040090528 B/op 10352 allocs/op
BenchmarkConcatDirect-8 2 685030600 ns/op 4040090768 B/op 10355 allocs/op
BenchmarkConcatDirect-8 2 817909650 ns/op 4040089808 B/op 10345 allocs/op
BenchmarkConcatDirect-8 2 600078100 ns/op 4040095664 B/op 10406 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 2925 419651 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2961 423899 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2714 422275 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2848 452255 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2612 454452 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 974 1158000 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1098 1068682 ns/op 4128192 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1042 1056570 ns/op 4128194 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1280 978213 ns/op 4128191 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1538 1162108 ns/op 4128190 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1744 700824 ns/op 4128203 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2235 759537 ns/op 4128201 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1556 736455 ns/op 4128204 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1592 825794 ns/op 4128201 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2263 717285 ns/op 4128203 B/op 29 allocs/op
PASS
ok golearn/tool_test 56.742sРезультат второго выполнения:
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 285074900 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 291517150 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 281901975 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 292320625 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 286723000 ns/op 4040056952 B/op 10002 allocs/op
BenchmarkConcatDirect-4 1 1188983000 ns/op 4040071856 B/op 10158 allocs/op
BenchmarkConcatDirect-4 1 1080713900 ns/op 4040070800 B/op 10147 allocs/op
BenchmarkConcatDirect-4 1 1203622300 ns/op 4040067344 B/op 10111 allocs/op
BenchmarkConcatDirect-4 1 1045291300 ns/op 4040070224 B/op 10141 allocs/op
BenchmarkConcatDirect-4 1 1123163300 ns/op 4040070032 B/op 10139 allocs/op
BenchmarkConcatDirect-8 2 790421300 ns/op 4040076656 B/op 10208 allocs/op
BenchmarkConcatDirect-8 2 659047300 ns/op 4040079488 B/op 10237 allocs/op
BenchmarkConcatDirect-8 2 712991800 ns/op 4040077184 B/op 10213 allocs/op
BenchmarkConcatDirect-8 2 706605350 ns/op 4040078000 B/op 10222 allocs/op
BenchmarkConcatDirect-8 2 656195700 ns/op 4040085248 B/op 10297 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 2726 386412 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3439 335358 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3376 338957 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3870 326301 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 4285 339596 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1663 671535 ns/op 4128187 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1507 744885 ns/op 4128191 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1353 1097800 ns/op 4128187 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1388 1006019 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1635 993764 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1332 783599 ns/op 4128198 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1818 729821 ns/op 4128202 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1398 780614 ns/op 4128202 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1526 750513 ns/op 4128204 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2164 704798 ns/op 4128204 B/op 29 allocs/op
PASS
ok golearn/tool_test 50.387sЗатем используем benchstat для сравнения:
$ benchstat old.txt new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
ConcatDirect-2 299.0m ± ∞ ¹ 286.7m ± ∞ ¹ ~ (p=0.310 n=5)
ConcatDirect-4 894.7m ± ∞ ¹ 1123.2m ± ∞ ¹ +25.53% (p=0.008 n=5)
ConcatDirect-8 684.0m ± ∞ ¹ 706.6m ± ∞ ¹ ~ (p=0.548 n=5)
ConcatBuilder-2 423.9µ ± ∞ ¹ 339.0µ ± ∞ ¹ -20.04% (p=0.008 n=5)
ConcatBuilder-4 1068.7µ ± ∞ ¹ 993.8µ ± ∞ ¹ ~ (p=0.151 n=5)
ConcatBuilder-8 736.5µ ± ∞ ¹ 750.5µ ± ∞ ¹ ~ (p=0.841 n=5)
geomean 19.84m 19.65m -0.98%
¹ need >= 6 samples for confidence interval at level 0.95
│ old.txt │ new.txt │
│ B/op │ B/op vs base │
ConcatDirect-2 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ ~ (p=1.000 n=5)
ConcatDirect-4 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ -0.00% (p=0.008 n=5)
ConcatDirect-8 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ -0.00% (p=0.008 n=5)
ConcatBuilder-2 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-4 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=0.079 n=5)
ConcatBuilder-8 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=0.952 n=5)
geomean 123.2Mi 123.2Mi -0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal
│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
ConcatDirect-2 9.999k ± ∞ ¹ 9.999k ± ∞ ¹ ~ (p=1.000 n=5)
ConcatDirect-4 10.22k ± ∞ ¹ 10.14k ± ∞ ¹ -0.74% (p=0.008 n=5)
ConcatDirect-8 10.35k ± ∞ ¹ 10.22k ± ∞ ¹ -1.26% (p=0.008 n=5)
ConcatBuilder-2 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-4 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-8 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
geomean 543.6 541.7 -0.33%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equalИз результата видно, что benchstat разделил данные на три группы: время, использование памяти и количество выделений памяти. geomean — среднее значение, p — уровень значимости выборки, критический интервал обычно 0.05. Значение выше 0.05 менее достоверно. Возьмём одну строку данных:
│ sec/op │ sec/op vs base │
ConcatDirect-4 894.7m ± ∞ ¹ 1123.2m ± ∞ ¹ +25.53% (p=0.008 n=5)Можно видеть, что время выполнения old составило 894.7ms, время выполнения new — 1123.2ms, что на 25.53% больше.
Нечёткое тестирование (Fuzzing)
Нечёткое тестирование — это новая функция, представленная в GO1.18, являющаяся улучшением модульных тестов и бенчмарков. Разница в том, что для первых двух тестов данные нужно писать вручную, а нечёткое тестирование может генерировать случайные тестовые данные из корпуса. О нечётком тестировании в Go можно узнать больше на Go Fuzzing. Преимущество нечёткого тестирования в том, что по сравнению с фиксированными тестовыми данными случайные данные лучше тестируют граничные условия программы. Ниже рассмотрим пример из официального руководства. На этот раз нужно протестировать функцию разворота строки. Сначала создадим файл /tool/strings.go и запишем следующий код:
package tool
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}Создадим файл нечёткого тестирования /tool_test/fuzz_tool_test.go и запишем следующий код:
package tool
import (
"golearn/tool"
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
testdata := []string{"hello world!", "nice to meet you", "good bye!"}
for _, data := range testdata {
f.Add(data)
}
f.Fuzz(func(t *testing.T, str string) {
first := tool.Reverse(str)
second := tool.Reverse(first)
t.Logf("str:%q,first:%q,second:%q", str, first, second)
if str != second {
t.Errorf("before: %q, after: %q", str, second)
}
if utf8.ValidString(str) && !utf8.ValidString(first) {
t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
}
})
}В нечётком тестировании сначала нужно добавить данные в семенной корпус. В примере используется f.Add() для добавления, что помогает генерировать случайные тестовые данные. Затем используется f.Fuzz(fn) для тестирования. Сигнатура функции:
func (f *F) Fuzz(ff any)
func (f *F) Add(args ...any)fn похож на логику функции модульного теста. Первый параметр функции должен быть t *testing.T, за ним следуют желаемые параметры. Поскольку передаваемая строка непредсказуема, здесь используется метод двукратного разворота для проверки. Выполним следующую команду:
$ go test -run Fuzz -v
=== RUN FuzzReverse
=== RUN FuzzReverse/seed#0
fuzz_tool_test.go:18: str:"hello world!",first:"!dlrow olleh",second:"hello world!"
=== RUN FuzzReverse/seed#1
fuzz_tool_test.go:18: str:"nice to meet you",first:"uoy teem ot ecin",second:"nice to meet you"
=== RUN FuzzReverse/seed#2
fuzz_tool_test.go:18: str:"good bye!",first:"!eyb doog",second:"good bye!"
--- PASS: FuzzReverse (0.00s)
--- PASS: FuzzReverse/seed#0 (0.00s)
--- PASS: FuzzReverse/seed#1 (0.00s)
--- PASS: FuzzReverse/seed#2 (0.00s)
PASS
ok golearn/tool_test 0.539sКогда параметр не содержит -fuzz, случайные тестовые данные не генерируются, в функцию тестирования передаются только данные из корпуса. Из результата видно, что все тесты прошли. Такое использование эквивалентно модульному тестированию, но на самом деле есть проблема. Ниже добавим параметр -fuzz и выполним снова:
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/217 completed
fuzz: minimizing 91-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 15/217 completed
--- FAIL: FuzzReverse (0.13s)
--- FAIL: FuzzReverse (0.00s)
fuzz_tool_test.go:18: str:"𐑄",first:"\x84\x91\x90\xf0",second:"𐑄"
fuzz_tool_test.go:23: Reverse produced invalid UTF-8 string "𐑄" "\x84\x91\x90\xf0"
Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2
=== NAME
FAIL
exit status 1
FAIL golearn/tool_test 0.697sTIP
Неудачные случаи в нечётком тестировании выводятся в файл корпуса в директории testdata текущей тестовой папки. Например, в приведённом выше примере:
Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2testdata\fuzz\FuzzReverse\d856c981b6266ba2 — это путь к файлу корпуса. Содержимое файла:
go test fuzz v1
string("𐑄")Можно видеть, что на этот раз тесты не прошли, потому что строка после разворота стала не в формате UTF-8. Таким образом, нечёткое тестирование обнаружило эту проблему. Поскольку некоторые символы занимают больше одного байта, их разворот по байтам определённо приведёт к кракозябрам. Поэтому изменим исходный код тестируемой функции следующим образом, преобразовав строку в []rune, чтобы избежать вышеупомянутой проблемы:
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}Далее запустим непосредственно тест на основе неудачного случая предыдущего нечёткого тестирования:
$ go test -run=FuzzReverse/d856c981b6266ba2 -v
=== RUN FuzzReverse
=== RUN FuzzReverse/d856c981b6266ba2
fuzz_tool_test.go:18: str:"𐑄",first:"𐑄",second:"𐑄"
--- PASS: FuzzReverse (0.00s)
--- PASS: FuzzReverse/d856c981b6266ba2 (0.00s)
PASS
ok golearn/tool_test 0.033sМожно видеть, что на этот раз тесты прошли. Снова выполним нечёткое тестирование, чтобы проверить, есть ли ещё проблемы:
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: minimizing 70-byte failing input file
failure while testing seed corpus entry: FuzzReverse/d97214ce235bfcf5
fuzz: elapsed: 0s, gathering baseline coverage: 2/219 completed
--- FAIL: FuzzReverse (0.15s)
--- FAIL: FuzzReverse (0.00s)
fuzz_tool_test.go:18: str:"\xe4",first:"",second:""
fuzz_tool_test.go:20: before: "\xe4", after: ""
=== NAME
FAIL
exit status 1
FAIL golearn/tool_test 0.184sМожно обнаружить ещё одну ошибку. На этот раз проблема в том, что после двух разворотов строки не равны. Исходный символ — \xe4, ожидаемый результат — 4ex\, но результат — кракозябры:
func main() {
fmt.Println("\xe4")
fmt.Println([]byte("\xe4"))
fmt.Println([]rune("\xe4"))
fmt.Printf("%q\n", "\xe4")
fmt.Printf("%x\n", "\xe4")
}Результат выполнения:
[65533]
"\xe4"
"\xe4"
e4Причина в том, что \xe4 представляет один байт, но не является допустимой последовательностью UTF-8 (в кодировке UTF-8 \xe4 — начало трёхбайтового символа, но отсутствуют два байта). При преобразовании в []rune Golang автоматически превращает его в []rune{"\uFFFD"}. После разворота остаётся []rune{"\uFFFD"}. При преобразовании обратно в string этот Unicode-символ заменяется на его UTF-8 кодировку \xef\xbf\xbd. Поэтому одно из решений — если передаётся недопустимая UTF-8 строка, сразу возвращать ошибку:
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}Тестовый код также нужно немного изменить:
func FuzzReverse(f *testing.F) {
testdata := []string{"hello world!", "nice to meet you", "good bye!"}
for _, data := range testdata {
f.Add(data)
}
f.Fuzz(func(t *testing.T, str string) {
first, err := tool.Reverse(str)
if err != nil {
t.Skip()
}
second, err := tool.Reverse(first)
if err != nil {
t.Skip()
}
t.Logf("str:%q,first:%q,second:%q", str, first, second)
if str != second {
t.Errorf("before: %q, after: %q", str, second)
}
if utf8.ValidString(str) && !utf8.ValidString(first) {
t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
}
})
}Когда функция разворота возвращает error, пропускаем тест. Снова выполним нечёткое тестирование:
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: elapsed: 0s, gathering baseline coverage: 219/219 completed, now fuzzing with 16 workers
fuzz: elapsed: 3s, execs: 895571 (297796/sec), new interesting: 32 (total: 251)
fuzz: elapsed: 6s, execs: 1985543 (363120/sec), new interesting: 37 (total: 256)
fuzz: elapsed: 9s, execs: 3087837 (367225/sec), new interesting: 38 (total: 257)
fuzz: elapsed: 12s, execs: 4090817 (335167/sec), new interesting: 40 (total: 259)
fuzz: elapsed: 15s, execs: 5132580 (346408/sec), new interesting: 44 (total: 263)
fuzz: elapsed: 18s, execs: 6248486 (372185/sec), new interesting: 45 (total: 264)
fuzz: elapsed: 21s, execs: 7366827 (373305/sec), new interesting: 46 (total: 265)
fuzz: elapsed: 24s, execs: 8439803 (358059/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 27s, execs: 9527671 (361408/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 30s, execs: 10569473 (348056/sec), new interesting: 48 (total: 267)
fuzz: elapsed: 30s, execs: 10569473 (0/sec), new interesting: 48 (total: 267)
--- PASS: FuzzReverse (30.16s)
=== NAME
PASS
ok golearn/tool_test 30.789sТеперь можно получить более полный вывод логов нечёткого тестирования. Объяснение некоторых понятий:
- elapsed: время, прошедшее после завершения одного раунда
- execs: общее количество запущенных входов, 297796/sec означает количество входов в секунду
- new interesting: общее количество «интересных» входов, добавленных в корпус во время тестирования. (Интересный вход — это вход, который может расширить покрытие кода за пределы диапазона, покрываемого существующим корпусом. По мере расширения покрытия его тенденция роста в целом будет замедляться.)
TIP
Без параметра -fuzztime, ограничивающего время, нечёткое тестирование будет выполняться бесконечно.
Поддерживаемые типы
В Go Fuzz поддерживаются следующие типы:
string,[]byteint,int8,int16,int32/rune,int64uint,uint8/byte,uint16,uint32,uint64float32,float64bool
