Метод testing.F.Fuzz

Метод testing.F.Fuzz запускает фаззинг-тест, который многократно вызывает фаззинг-функцию с случайно сгенерированными значениями. Фаззинг-тестирование помогает обнаруживать ошибки и паники, которые возникают при обработке некорректных или неожиданных входных данных. Первым параметром метод принимает фаззинг-функцию (func(f *testing.F)), которая содержит логику тестирования и вызывается с различными значениями.

Синтаксис

gofunc Fuzz(f *testing.F)

Метод Fuzz вызывается на объекте типа *testing.F. Внутри фаззинг-функции необходимо использовать метод f.Add для добавления "seed" (начальных) корпусов и f.Fuzz для определения основной логики проверки.

Пример

Давайте напишем фаззинг-тест для функции, которая принимает строку и проверяет, является ли она палиндромом. Мы добавим несколько начальных значений и определим логику проверки:

gopackage main

import (
  "testing"
)

func IsPalindrome(s string) bool {
  for i := 0; i < len(s)/2; i++ {
    if s[i] != s[len(s)-1-i] {
      return false
    }
  }
  return true
}

func FuzzIsPalindrome(f *testing.F) {
  // Добавляем seed корпусы
  f.Add("abcba")
  f.Add("abcd")
  f.Add("a")
  f.Add("")

  f.Fuzz(func(t *testing.T, s string) {
    result := IsPalindrome(s)
    if result && len(s) > 1 {
      // Проверяем, что палиндром читается одинаково с обеих сторон
      for i := 0; i < len(s)/2; i++ {
        if s[i] != s[len(s)-1-i] {
          t.Errorf("IsPalindrome(%q) = true, but it's not a palindrome", s)
        }
      }
    }
  })
}

Для запуска фаззинг-теста используйте команду:

gogo test -fuzz=FuzzIsPalindrome

Результат выполнения команды (пример):

go"fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0"
"fuzz: elapsed: 1s, execs: 100 (100/sec), new interesting: 2"
"fuzz: elapsed: 2s, execs: 200 (100/sec), new interesting: 3"
"PASS"

Пример

Фаззинг-тест может принимать несколько типов данных одновременно. Например, функция, которая конкатенирует две строки и проверяет результат:

gopackage main

import (
  "testing"
)

func Concat(a, b string) string {
  return a + b
}

func FuzzConcat(f *testing.F) {
  // Добавляем seed корпусы с несколькими параметрами
  f.Add("hello", "world")
  f.Add("foo", "bar")
  f.Add("", "test")

  f.Fuzz(func(t *testing.T, a, b string) {
    result := Concat(a, b)
    expected := a + b
    if result != expected {
      t.Errorf("Concat(%q, %q) = %q, expected %q", a, b, result, expected)
    }
  })
}

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

go"fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0"
"fuzz: elapsed: 1s, execs: 50 (50/sec), new interesting: 1"
"fuzz: elapsed: 2s, execs: 100 (50/sec), new interesting: 1"
"PASS"

Фаззинг-тест автоматически генерирует новые комбинации входных данных, чтобы найти крайние случаи, которые могут вызвать панику или неверное поведение.

Пример

Давайте рассмотрим, как фаззинг-тест может найти ошибку в функции, которая делит два числа. Мы используем тип данных int:

gopackage main

import (
  "testing"
)

func Divide(a, b int) int {
  return a / b // Может вызвать панику при b == 0
}

func FuzzDivide(f *testing.F) {
  // Добавляем seed корпусы
  f.Add(10, 2)
  f.Add(5, 1)
  f.Add(0, 1)

  f.Fuzz(func(t *testing.T, a, b int) {
    if b == 0 {
      t.Skip("Skipping division by zero")
    }
    result := Divide(a, b)
    expected := a / b
    if result != expected {
      t.Errorf("Divide(%d, %d) = %d, expected %d", a, b, result, expected)
    }
  })
}

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

go"fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0"
"fuzz: elapsed: 1s, execs: 100 (100/sec), new interesting: 2"
"--- PASS: FuzzDivide (0.00s)"
"PASS"

Фаззинг-тест автоматически пропускает случаи с делением на ноль, но если бы мы не обрабатывали это, он бы нашел панику и сообщил о ней.

Смотрите также

  • метод testing.F.Add,
    который добавляет seed корпусы для фаззинг-теста
  • метод testing.F.Skip,
    который пропускает текущий фаззинг-тест
  • тип testing.T,
    который используется для обычных модульных тестов
  • тип testing.B,
    который используется для бенчмарков