Skip to content

การทดสอบ

สำหรับผู้พัฒนาแล้ว การทดสอบที่ดีสามารถค้นพบข้อผิดพลาดในโปรแกรมได้ล่วงหน้า หลีกเลี่ยงภาระทางจิตใจที่เกิดจาก 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 โค้ดมีดังนี้

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สถิติสถานการณ์การบล็อกของ 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:

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 ดำเนินการ

การทดสอบหน่วย

การทดสอบหน่วยคือการทดสอบหน่วยที่เล็กที่สุดที่ทดสอบได้ในซอฟต์แวร์ ขนาดของหน่วยขึ้นอยู่กับผู้พัฒนา อาจเป็น struct หนึ่งตัว หรือเป็นแพ็กเกจหนึ่งแพ็กเกจ หรืออาจเป็นฟังก์ชันหนึ่งฟังก์ชัน หรือเป็นประเภทหนึ่งประเภท ต่อไปยังคงใช้ตัวอย่างเพื่อแสดง ก่อนอื่นสร้างไฟล์ /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 เป็น struct ที่แพ็กเกจ 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 จะเปิด goroutine ใหม่สำหรับดำเนินการทดสอบย่อย บล็อก等待จนกว่าฟังก์ชัน 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

สไตล์ตาราง

ในการทดสอบหน่วยข้างต้น ข้อมูลนำเข้าการทดสอบล้วนประกาศตัวแปรทีละตัวด้วยตนเอง เมื่อปริมาณข้อมูลน้อยก็ไม่เป็นไร แต่หากต้องการทดสอบหลายชุดข้อมูล ก็ไม่น่าจะไปประกาศตัวแปรเพื่อสร้างข้อมูลทดสอบอีก ดังนั้นโดยทั่วไปแล้วจะพยายามใช้รูปแบบ slice ของ struct มากที่สุด struct เป็น struct ไม่ระบุชื่อที่ประกาศชั่วคราว เพราะสไตล์การเขียนโค้ดเช่นนี้看起来就跟ตาราง一样 ดังนั้นจึงเรียกว่า 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 บางอย่าง ผลลัพธ์ของแต่ละเคสทดสอบด้านล่างแยกด้วยชื่อการทดสอบเบนช์มาร์กแต่ละตัว ตัวเลข 2 ใน BenchmarkConcatDirect-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%

การทดสอบฟัซ

การทดสอบฟัซเป็นฟีเจอร์ใหม่ที่ 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("𐑄")

จะเห็นว่าครั้งนี้ไม่ผ่าน เหตุผลคือสตริงหลังจากกลับด้านกลายเป็นรูปแบบที่ไม่ใช่ utf8 ดังนั้นผ่านการทดสอบฟัซก็ค้นพบปัญหานี้ เนื่องจากบางตัวอักษรใช้มากกว่าหนึ่งไบต์ หากกลับด้านโดยใช้หน่วยเป็นไบต์肯定เป็น乱码 ดังนั้นแก้ไขซอร์สโค้ดรอทดสอบเป็นดังนี้ แปลงสตริงเป็น []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"
e4

สาเหตุคือ \xe4 แสดงถึงหนึ่งไบต์ แต่ไม่ใช่ลำดับ UTF-8 ที่มีประสิทธิภาพ (ในการเข้ารหัส UTF-8 \xe4 เป็นจุดเริ่มต้นของตัวอักษรสามไบต์ แต่ขาดสองไบต์ด้านหลัง) เมื่อแปลงเป็น []rune Golang จะแปลงเป็น []rune{"\uFFFD"} ที่มีตัวอักษร Unicode เดียวโดยอัตโนมัติ หลังจากกลับด้านแล้วยังเป็น []rune{"\uFFFD"} เมื่อแปลงกลับเป็น string ตัวอักษร Unicode นี้จะถูกแทนที่ด้วยการเข้ารหัส UTF-8 ของมัน \xef\xbf\xbd ดังนั้นวิธีแก้วิธีหนึ่งคือหากสตริงที่ส่งเข้าไม่ใช่ utf8 ส่งคืนข้อผิดพลาดโดยตรง:

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