Что такое фаззинг

Нечеткое тестирование или нечеткое тестирование — это метод предоставления случайных неожиданных входных данных вашим программам для проверки возможных сбоев или пограничных случаев. Фаззинг может пролить свет на некоторые логические ошибки или проблемы с производительностью, поэтому его всегда стоит добавлять в код, где важны стабильность и производительность.

Go проекты для фаззинга

В настоящее время существует несколько хорошо поддерживаемых проектов для фаззинга:

Но мы не будем рассматривать их в этой статье, так как у нас есть отличные новости, так как команда Go приняла предложение добавить в язык поддержку нечеткого тестирования. Он будет доступен в стандартном тулчейне Go 1.18 — docs.

Установите Go 1.18

На момент написания этого поста Go 1.18 находится только в бета-версии, поэтому давайте сначала установим его с помощью gotip.

go install golang.org/dl/gotip@latest
gotip download

Примечание: вероятно, когда вы читаете это, Go 1.18 уже выпущен, поэтому вы можете обновить свою команду go обычным способом.

Наша «домашняя» функция

Давайте напишем простую функцию, для которой позже мы добавим фаззинг. И мы намеренно добавим некоторые ошибки.

Функция Equal будет сравнивать два фрагмента байтов поэлементно.

package fuzztestingingo
func Equal(a []byte, b []byte) bool {
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

Написание фазз-теста

  1. Создайте файл equal_test.go
  2. Включим простой регулярный тест
package fuzztestingingo
import "testing"
func TestEqual(t *testing.T) {
	if !Equal([]byte{'f', 'u', 'z', 'z'}, []byte{'f', 'u', 'z', 'z'}) {
		t.Error("expected true, got false")
	}
}
gotip test .
ok github.com/plutov/packagemain/23-fuzz-testing-in-go	0.922s

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

Нечеткие тесты могут быть включены в ваши обычные файлы _test.go с помощью функций, начинающихся с Fuzz, которые принимают новый тип *testing.F.

func FuzzEqual(f *testing.F) {
    // target, can be only one per test
	// values of a and b will be auto-generated
	f.Fuzz(func(t *testing.T, a []byte, b []byte) {
		Equal(a, b)
	})
}

Примечание. В тесте может быть только одна цель.

Чтобы включить фаззинг, мы должны запустить go test с флагом -fuzz:

gotip test -fuzz .
fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed
failure while testing seed corpus entry: FuzzEqual/84ed65595ad05a58e293dbf423c1a816b697e2763a29d7c37aa476d6eef6fd60
fuzz: elapsed: 0s, gathering baseline coverage: 1/2 completed
--- FAIL: FuzzEqual (0.02s)
    --- FAIL: FuzzEqual (0.00s)
        testing.go:1349: panic: runtime error: index out of range [0] with length 0

Исправление функции «домашнее животное»

Мы нашли свою ошибку, так как наш код не проверяет размер среза. Давайте исправим это и снова запустим фазз.

package fuzztestingingo
func Equal(a []byte, b []byte) bool {
    // if length is not the same - slices are not equal
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
gotip test -fuzz .
fuzz: elapsed: 0s, gathering baseline coverage: 0/11 completed
fuzz: elapsed: 0s, gathering baseline coverage: 11/11 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 542957 (180982/sec), new interesting: 0 (total: 11)
fuzz: elapsed: 6s, execs: 1035678 (164216/sec), new interesting: 0 (total: 11)
...

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

gotip test -fuzz=. -fuzztime=5s .
fuzz: elapsed: 0s, gathering baseline coverage: 0/10 completed
fuzz: elapsed: 0s, gathering baseline coverage: 10/10 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 474778 (158251/sec), new interesting: 0 (total: 10)
fuzz: elapsed: 5s, execs: 729255 (121223/sec), new interesting: 0 (total: 10)
PASS
ok  	github.com/plutov/packagemain/23-fuzz-testing-in-go	5.557s

Теперь давайте рассмотрим результат фаззинга, есть несколько метрик:

  • истекло: количество времени, прошедшее с момента начала процесса
  • execs: общее количество входных данных, которые были выполнены для цели fuzz.
  • новое интересное: общее количество «интересных» входных данных, которые были добавлены к сгенерированному корпусу во время выполнения этого фаззинга.

Имейте в виду, что фаззинг может потреблять много памяти и может повлиять на производительность вашей машины во время ее работы, поэтому вам следует быть осторожным при использовании фаззинга в конвейере CI.