Featured image of post [golang] Fuzz test in Go

[golang] Fuzz test in Go

Fuzz test

Fuzz 테스팅은 무작위 데이터를 반복적으로 입력하여 동작의 실패를 유도하여 취약점을 찾아내는 테스트 기법입니다. Go 언어에서는 1.18 버전 부터 언어 차원에서 Fuzz 테스팅을 지원하고있습니다.

Fuzz test 작성 규칙

  • Fuzz 테스트 함수명은 Fuzz로 시작합니다.
  • 테스트 함수는 *testing.F 만을 매개변수로 받습니다.

예시

Fuzz 테스팅을 통해 함수를 개선하는 과정을 진행 해 보겠습니다.

최초 작성된 함수

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func Equal(a, b string) bool {
	ab, bb := []byte(a), []byte(b)

	for i, v := range ab {
		if bb[i] != v {
			return false
		}
	}
	return true
}

위 함수는 쉬운 길을 내버려두고 두 string을 []byte 로 변환하여 비교하는 함수입니다. 이 함수를 테스트 해 봅시다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func TestEqual(t *testing.T) {
	tests := []struct {
		a, b   string
		result bool
	}{
		{"abc", "abc", true},
		{"abc", "abd", false},
	}

	for _, test := range tests {
		result := Equal(test.a, test.b)
		if result != test.result {
			t.Errorf("Equal(%v, %v) = %v, want %v", test.a, test.b, result, test.result)
		}
	}
}

이상적인 값이 들어가는 두 개의 케이스를 검증 하는 테스트를 작성했습니다.

테스트 통과!

테스트도 통과 했네요! 하지만 우리는 예외적인 입력값을 테스트 해보기 위해 Fuzz 테스트를 추가 해 보기로 했습니다.

Fuzz 테스트

1
2
3
4
5
func FuzzEqual(f *testing.F) {
	f.Fuzz(func(t *testing.T, a, b string) {
		Equal(a, b)
	})
}

Fuzz 테스트는 아래의 명령어로 쉽게 실행할 수 있습니다.

1
go test -fuzz .

실행 결과

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 ❯ go test -fuzz .
fuzz: elapsed: 0s, gathering baseline coverage: 0/4 completed
fuzz: elapsed: 0s, gathering baseline coverage: 4/4 completed, now fuzzing with 10 workers
fuzz: minimizing 56-byte failing input file
fuzz: elapsed: 0s, minimizing
--- FAIL: FuzzEqual (0.04s)
    --- FAIL: FuzzEqual (0.00s)
        testing.go:1485: panic: runtime error: index out of range [1] with length 1

...(생략)

    Failing input written to testdata/fuzz/FuzzEqual/469ce7fee3611e98
    To re-run:
    go test -run=FuzzEqual/469ce7fee3611e98

Fuzz 테스트에 실패 했네요! Fuzz 테스트에 실패 한 경우 실행결과 하단에 나오는 testdata 폴더 아래에 저장된 파일에 실패했을 때 사용된 값이 기록됩니다.

1
2
3
go test fuzz v1
string("a0")
string("a")

testdata에 기록된 실패한 케이스와 에러메시지를 보니 매개변수로 들어오는 두 string 의 길이가 다를 경우 panic이 발생하는걸 확인 할 수 있었습니다.

함수 개선

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func Equal(a, b string) bool {
	if len(a) != len(b) {
		return false
	}
	
	ab, bb := []byte(a), []byte(b)

	for i, v := range ab {
		if bb[i] != v {
			return false
		}
	}
	return true
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func TestEqual(t *testing.T) {
	tests := []struct {
		a, b   string
		result bool
	}{
		{"abc", "abc", true},
		{"abc", "abd", false},
		{"a0", "a", false},
	}

	for _, test := range tests {
		result := Equal(test.a, test.b)
		if result != test.result {
			t.Errorf("Equal(%v, %v) = %v, want %v", test.a, test.b, result, test.result)
		}
	}
}

이번 Fuzz 테스트로 발견된 이슈로 매개변수로 들어온 두 string의 길이가 같은지 검증하는 코드가 추가되고, 이에 대응하는 Test Case 도 추가하여 코드를 개선하였습니다.

정리

Fuzz 테스트는 예외처리가 부족하여 나타날 수 있는 Panic 을 방지하는데 유용하여 더 튼튼한 코드를 작성하는데에 도움을 줍니다. 예를들어 SQL을 변수와 함께 사용하는 경우가 있습니다.

다만 Fuzz 테스트는 오류가 나타날 때 까지 랜덤한 값을 무작위로 넣어본다는 테스트 특성상 계속 늘어질 수 있으므로 생산성을 위해 적절한 시간을 넣는 것이 중요합니다. 또한 컴퓨팅 파워를 많이 사용하므로 CI/CD 같은 자동화 프로세스에 넣기 전에는 주의 하여야 합니다.

Hugo로 만듦
JimmyStack 테마 사용 중