การทดสอบ
สำหรับผู้พัฒนาแล้ว การทดสอบที่ดีสามารถค้นพบข้อผิดพลาดในโปรแกรมได้ล่วงหน้า หลีกเลี่ยงภาระทางจิตใจที่เกิดจาก Bug เนื่องจากไม่ได้รับการบำรุงรักษาอย่างทันท่วงที ดังนั้นการเขียนการทดสอบให้ดีจึงจำเป็นอย่างยิ่ง Go ได้จัดเตรียมเครื่องมือบรรทัดคำสั่งที่สะดวกและใช้งานได้จริง go test สำหรับการทดสอบ สามารถเห็นการทดสอบในไลบรารีมาตรฐานและเฟรมเวิร์กโอเพนซอร์สจำนวนมาก เครื่องมือนี้ง่ายต่อการใช้ ปัจจุบันรองรับการทดสอบหลายประเภทดังนี้
- การทดสอบตัวอย่าง
- การทดสอบหน่วย
- การทดสอบเบนช์มาร์ก
- การทดสอบฟัซ
ใน Go API ส่วนใหญ่จัดเตรียมโดยไลบรารีมาตรฐาน 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 | สถิติสถานการณ์การบล็อกของ goroutine ในการทดสอบและเขียนลงไฟล์ |
-blockprofilerate n | ควบคุมความถี่สถิติการบล็อกของ goroutine ผ่านคำสั่ง 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 | ตั้งค่าสถิติสถานการณ์ที่ goroutine 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 ดำเนินการ
การทดสอบหน่วย
การทดสอบหน่วยคือการทดสอบหน่วยที่เล็กที่สุดที่ทดสอบได้ในซอฟต์แวร์ ขนาดของหน่วยขึ้นอยู่กับผู้พัฒนา อาจเป็น struct หนึ่งตัว หรือเป็นแพ็กเกจหนึ่งแพ็กเกจ หรืออาจเป็นฟังก์ชันหนึ่งฟังก์ชัน หรือเป็นประเภทหนึ่งประเภท ต่อไปยังคงใช้ตัวอย่างเพื่อแสดง ก่อนอื่นสร้างไฟล์ /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 เป็น struct ที่แพ็กเกจ 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 จะเปิด goroutine ใหม่สำหรับดำเนินการทดสอบย่อย บล็อก等待จนกว่าฟังก์ชัน 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สไตล์ตาราง
ในการทดสอบหน่วยข้างต้น ข้อมูลนำเข้าการทดสอบล้วนประกาศตัวแปรทีละตัวด้วยตนเอง เมื่อปริมาณข้อมูลน้อยก็ไม่เป็นไร แต่หากต้องการทดสอบหลายชุดข้อมูล ก็ไม่น่าจะไปประกาศตัวแปรเพื่อสร้างข้อมูลทดสอบอีก ดังนั้นโดยทั่วไปแล้วจะพยายามใช้รูปแบบ slice ของ struct มากที่สุด struct เป็น struct ไม่ระบุชื่อที่ประกาศชั่วคราว เพราะสไตล์การเขียนโค้ดเช่นนี้看起来就跟ตาราง一样 ดังนั้นจึงเรียกว่า 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 บางอย่าง ผลลัพธ์ของแต่ละเคสทดสอบด้านล่างแยกด้วยชื่อการทดสอบเบนช์มาร์กแต่ละตัว ตัวเลข 2 ใน BenchmarkConcatDirect-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%
การทดสอบฟัซ
การทดสอบฟัซเป็นฟีเจอร์ใหม่ที่ 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("𐑄")จะเห็นว่าครั้งนี้ไม่ผ่าน เหตุผลคือสตริงหลังจากกลับด้านกลายเป็นรูปแบบที่ไม่ใช่ utf8 ดังนั้นผ่านการทดสอบฟัซก็ค้นพบปัญหานี้ เนื่องจากบางตัวอักษรใช้มากกว่าหนึ่งไบต์ หากกลับด้านโดยใช้หน่วยเป็นไบต์肯定เป็น乱码 ดังนั้นแก้ไขซอร์สโค้ดรอทดสอบเป็นดังนี้ แปลงสตริงเป็น []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"
e4สาเหตุคือ \xe4 แสดงถึงหนึ่งไบต์ แต่ไม่ใช่ลำดับ UTF-8 ที่มีประสิทธิภาพ (ในการเข้ารหัส UTF-8 \xe4 เป็นจุดเริ่มต้นของตัวอักษรสามไบต์ แต่ขาดสองไบต์ด้านหลัง) เมื่อแปลงเป็น []rune Golang จะแปลงเป็น []rune{"\uFFFD"} ที่มีตัวอักษร Unicode เดียวโดยอัตโนมัติ หลังจากกลับด้านแล้วยังเป็น []rune{"\uFFFD"} เมื่อแปลงกลับเป็น string ตัวอักษร Unicode นี้จะถูกแทนที่ด้วยการเข้ารหัส UTF-8 ของมัน \xef\xbf\xbd ดังนั้นวิธีแก้วิธีหนึ่งคือหากสตริงที่ส่งเข้าไม่ใช่ utf8 ส่งคืนข้อผิดพลาดโดยตรง:
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
