Skip to content

Тестирование

Для разработчика хорошее тестирование позволяет заранее обнаружить ошибки в программе, избежать умственной нагрузки из-за возникновения багов в результате несвоевременного обслуживания, поэтому очень важно правильно писать тесты. 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:

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

И тестовый файл /test/example_test.go:

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:

sh
$ go test ./
PASS
ok      golearn/test    0.422s

./ означает текущую директорию. Go перекомпилирует все тестовые файлы в директории test, а затем выполнит все тестовые случаи. Из результата видно, что все тесты прошли успешно. После этого можно указать несколько директорий в качестве параметров. Например, следующая команда, очевидно, не найдёт тестовых файлов в главной директории проекта:

sh
$ go test ./ ../
ok      golearn/test
?       golearn [no test files]

TIP

Когда параметров несколько пакетов, Go не выполняет повторно успешно пройденные тесты. При выполнении в конце строки добавляется (cached), чтобы указать, что результат взят из кэша предыдущего выполнения. Go кэширует результаты тестов, когда флаги тестирования находятся в следующем наборе, иначе не кэширует:

-benchtime, -cpu,-list, -parallel, -run, -short, -timeout, -failfast, -v

Если вы хотите отключить кэширование, добавьте параметр -count=1.

Конечно, можно указать отдельный тестовый файл для выполнения:

sh
$ go test example_test.go
ok      command-line-arguments  0.457s

Или можно указать отдельный тестовый случай в тестовом файле. Например:

sh
$ go test -run ExampleSay
PASS
ok      golearn/test    0.038s

Все три вышеупомянутых случая завершили тестирование, но вывод слишком краток. В этом случае можно добавить параметр -v, чтобы сделать вывод более подробным. Например:

sh
$ 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, будут выполнены все типы тестов. Например:

sh
$ go test -bench .

Поэтому нужно использовать параметр -run для указания. Например, команда для выполнения только всех бенчмарков:

sh
$ 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:

go
// 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 и запишем следующий код:

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

Функция SayHello — это тестируемая функция. Затем создадим тестовый файл example_test.go и запишем следующий код:

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. Далее выполним команду тестирования и посмотрим результат:

sh
$ 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 и запишем следующий код:

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 или с префиксом пакета или функции, которую нужно протестировать:

go
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, который также помечает тест как неудачный, но первый продолжает выполнение после неудачи, а второй немедленно останавливает выполнение. В следующем примере изменим ожидаемый результат на неправильный:

go
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")
}

Выполним вышеупомянутые тесты, вывод будет следующим:

sh
$ 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, затем останавливает выполнение, но в следующем раунде тестирования он будет выполнен снова.

go
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
$ 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 для регистрации завершающей функции, которая будет выполнена в конце тестового случая:

go
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.462s

Helper

С помощью t.Helper() можно пометить текущую функцию как вспомогательную. Вспомогательная функция не выполняется как отдельный тестовый случай. При записи логов выводится номер строки вызывающего вспомогательную функцию, что делает анализ логов более точным и избегает лишней информации. Например, пример с t.Cleanup выше можно изменить на вспомогательную функцию:

go
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.464s

TIP

Все вышеупомянутые операции можно выполнять только в основном тесте, то есть в непосредственно выполняемом тестовом случае. Использование в подтесте вызовет panic.

Подтесты

В некоторых случаях может потребоваться тестирование других тестовых случаев внутри одного тестового случая. Такие вложенные тестовые случаи обычно называются подтестами. Метод t.Run() имеет следующую сигнатуру:

go
// Метод Run запускает новую горутину для выполнения подтеста, блокирует ожидание завершения функции f, затем возвращает
// Возвращаемое значение — успешность теста
func (t *T) Run(name string, f func(t *T)) bool

Вот пример:

go
func TestTool(t *testing.T) {
  t.Run("tool.Sum(10,101)", TestSum)
  t.Run("tool.Equal(10,101)", TestEqual)
}

Результат выполнения:

sh
$ 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() для пометки тестовых случаев как параллельно выполняемых. В этом случае порядок вывода будет неопределённым:

go
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():

go
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. Ниже приведён пример. Это пример ручного объявления нескольких переменных для создания тестовых данных. Если данных много, это не очень наглядно, поэтому изменим его на табличный стиль:

go
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)
  }
}

Изменённый код:

go
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 создадим две функции для двух способов конкатенации строк:

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:

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 и выполнив каждый тестовый случай дважды:

sh
$ 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 — это инструмент анализа производительности с открытым исходным кодом. В приведённых выше тестах производительности было только два образца. Как только образцов становится много, ручной анализ становится очень трудоёмким. Этот инструмент создан для решения проблем анализа производительности.

Сначала нужно загрузить этот инструмент:

sh
$ go install golang.org/x/perf/benchstat

Выполним бенчмарки дважды, на этот раз изменив количество образцов на 5, и выведем результаты в файлы old.txt и new.txt для сравнения. Результат первого выполнения:

sh
$ 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

Результат второго выполнения:

sh
$ 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 для сравнения:

sh
$ 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 и запишем следующий код:

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 и запишем следующий код:

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) для тестирования. Сигнатура функции:

go
func (f *F) Fuzz(ff any)

func (f *F) Add(args ...any)

fn похож на логику функции модульного теста. Первый параметр функции должен быть t *testing.T, за ним следуют желаемые параметры. Поскольку передаваемая строка непредсказуема, здесь используется метод двукратного разворота для проверки. Выполним следующую команду:

sh
$ 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 и выполним снова:

sh
$ 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.697s

TIP

Неудачные случаи в нечётком тестировании выводятся в файл корпуса в директории testdata текущей тестовой папки. Например, в приведённом выше примере:

Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2

testdata\fuzz\FuzzReverse\d856c981b6266ba2 — это путь к файлу корпуса. Содержимое файла:

go test fuzz v1
string("𐑄")

Можно видеть, что на этот раз тесты не прошли, потому что строка после разворота стала не в формате UTF-8. Таким образом, нечёткое тестирование обнаружило эту проблему. Поскольку некоторые символы занимают больше одного байта, их разворот по байтам определённо приведёт к кракозябрам. Поэтому изменим исходный код тестируемой функции следующим образом, преобразовав строку в []rune, чтобы избежать вышеупомянутой проблемы:

go
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)
}

Далее запустим непосредственно тест на основе неудачного случая предыдущего нечёткого тестирования:

sh
$ 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

Можно видеть, что на этот раз тесты прошли. Снова выполним нечёткое тестирование, чтобы проверить, есть ли ещё проблемы:

sh
$ 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\, но результат — кракозябры:

go
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 строка, сразу возвращать ошибку:

go
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
}

Тестовый код также нужно немного изменить:

go
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, пропускаем тест. Снова выполним нечёткое тестирование:

sh
$ 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, []byte
  • int, int8, int16, int32/rune, int64
  • uint, uint8/byte, uint16, uint32, uint64
  • float32, float64
  • bool

Golang by www.golangdev.cn edit