แยกการทดสอบหน่วยและการทดสอบการรวมใน Go


105

มีแนวทางปฏิบัติที่ดีที่สุดสำหรับการแยกการทดสอบหน่วยและการทดสอบบูรณาการใน GoLang (เป็นพยาน) หรือไม่ ฉันมีการทดสอบหน่วยหลายอย่าง (ซึ่งไม่ต้องพึ่งพาทรัพยากรภายนอกใด ๆ จึงทำงานได้เร็วมาก) และการทดสอบการรวม (ซึ่งอาศัยทรัพยากรภายนอกใด ๆ จึงทำงานช้าลง) ดังนั้นฉันต้องการให้สามารถควบคุมได้ว่าจะรวมการทดสอบการรวมเข้าด้วยกันเมื่อฉันพูดgo testหรือไม่

เทคนิคที่ตรงไปตรงมาที่สุดดูเหมือนจะเป็นการกำหนดแฟล็ก -integrate ใน main:

var runIntegrationTests = flag.Bool("integration", false
    , "Run the integration tests (in addition to the unit tests)")

จากนั้นเพิ่ม if-statement ที่ด้านบนของการทดสอบการรวมทุกครั้ง:

if !*runIntegrationTests {
    this.T().Skip("To run this test, use: go test -integration")
}

นี่คือสิ่งที่ดีที่สุดที่ฉันทำได้หรือไม่? ฉันค้นหาเอกสารประกอบการเป็นพยานเพื่อดูว่าอาจมีหลักการตั้งชื่อหรือสิ่งที่ทำให้ฉันบรรลุผลสำเร็จ แต่ก็ไม่พบอะไรเลย ฉันพลาดอะไรไปรึเปล่า?


2
ฉันคิดว่า stdlib ใช้ -short เพื่อปิดการใช้งานการทดสอบที่เข้าสู่เครือข่าย (และสิ่งอื่น ๆ ที่ใช้เวลานานด้วย) วิธีอื่น ๆ ที่ชาญฉลาดของคุณก็ดูโอเค
Volker

-short เป็นตัวเลือกที่ดีเช่นเดียวกับแฟล็กบิลด์ที่กำหนดเองของคุณ แต่แฟล็กของคุณไม่จำเป็นต้องอยู่ในสถานะหลัก หากคุณกำหนด var เป็นvar integration = flag.Bool("integration", true, "Enable integration testing.")ภายนอกฟังก์ชันตัวแปรจะแสดงในขอบเขตแพ็กเกจและแฟ
ล็ก

คำตอบ:


163

@ Ainar-G แนะนำรูปแบบที่ยอดเยี่ยมหลายประการในการแยกการทดสอบ

แนวทางปฏิบัติ Go ชุดนี้จาก SoundCloudแนะนำให้ใช้แท็กบิลด์ ( อธิบายไว้ในส่วน "ข้อ จำกัด การสร้าง" ของแพ็กเกจบิลด์ ) เพื่อเลือกว่าจะรันการทดสอบใด:

เขียน integration_test.go และให้ build tag ของการรวม กำหนดแฟล็ก (ส่วนกลาง) สำหรับสิ่งต่างๆเช่นที่อยู่บริการและเชื่อมต่อสตริงและใช้ในการทดสอบของคุณ

// +build integration

var fooAddr = flag.String(...)

func TestToo(t *testing.T) {
    f, err := foo.Connect(*fooAddr)
    // ...
}

ไปทดสอบสร้างแท็กเช่นเดียวกับ go build ดังนั้นคุณสามารถโทรgo test -tags=integrationได้ นอกจากนี้ยังสังเคราะห์แพ็กเกจหลักซึ่งเรียกว่า flag.Parse ดังนั้นแฟล็กใด ๆ ที่ประกาศและมองเห็นได้จะถูกประมวลผลและพร้อมใช้งานสำหรับการทดสอบของคุณ

เป็นตัวเลือกที่คล้ายกันคุณยังสามารถมีการทดสอบการรวมดำเนินการโดยเริ่มต้นโดยใช้เงื่อนไขที่สร้างแล้วปิดการใช้งานตามความต้องการโดยการเรียกใช้// +build !unitgo test -tags=unit

ความคิดเห็น @adamc:

สำหรับใครก็ตามที่พยายามใช้บิลด์แท็กสิ่งสำคัญ// +build testคือความคิดเห็นต้องเป็นบรรทัดแรกในไฟล์ของคุณและคุณต้องใส่บรรทัดว่างหลังความคิดเห็นมิฉะนั้น-tagsคำสั่งจะไม่สนใจคำสั่ง

นอกจากนี้แท็กที่ใช้ในการสร้างข้อคิดเห็นต้องไม่มีเครื่องหมายขีดกลางแม้ว่าจะอนุญาตให้มีเครื่องหมายขีดล่างก็ตาม ตัวอย่างเช่น// +build unit-testsจะไม่ทำงานในขณะที่// +build unit_testsจะ


1
ฉันใช้สิ่งนี้มาระยะหนึ่งแล้วและเป็นวิธีการที่ง่ายและสมเหตุสมผลที่สุด
Ory Band

1
หากคุณมีการทดสอบหน่วยในแพ็คเกจเดียวกันคุณต้องตั้งค่า// + build unitในการทดสอบหน่วยและใช้หน่วย -tag เพื่อทำการทดสอบ
LeoCBS

2
@ Tyler.z.yang คุณสามารถให้ลิงค์หรือรายละเอียดเพิ่มเติมเกี่ยวกับการเลิกใช้งานแท็กได้หรือไม่? ฉันไม่พบข้อมูลดังกล่าว ฉันใช้แท็กกับ go1.8 สำหรับวิธีที่อธิบายไว้ในคำตอบและสำหรับการล้อเลียนประเภทและฟังก์ชันในการทดสอบ เป็นทางเลือกที่ดีสำหรับอินเทอร์เฟซที่ฉันคิด
Alexander I. Grafov

2
สำหรับใครก็ตามที่พยายามใช้บิลด์แท็กสิ่งสำคัญ// +buildคือความคิดเห็นทดสอบคือบรรทัดแรกในไฟล์ของคุณและคุณต้องใส่บรรทัดว่างหลังความคิดเห็นมิฉะนั้น-tagsคำสั่งจะละเว้นคำสั่ง นอกจากนี้แท็กที่ใช้ในการสร้างข้อคิดเห็นต้องไม่มีเครื่องหมายขีดกลางแม้ว่าจะอนุญาตให้มีเครื่องหมายขีดล่างก็ตาม ยกตัวอย่างเช่น// +build unit-testsจะไม่ทำงานในขณะที่// +build unit_testsประสงค์
adamc

6
วิธีจัดการสัญลักษณ์แทน go test -tags=integration ./...ไม่ทำงานมันไม่สนใจแท็ก
Erika Dsouza

60

เพื่ออธิบายรายละเอียดเกี่ยวกับความคิดเห็นของฉันเกี่ยวกับคำตอบที่ยอดเยี่ยมของ @ Ainar-G ในช่วงปีที่ผ่านมาฉันใช้การผสมผสานระหว่าง -shortกับIntegrationตั้งชื่อการประชุมเพื่อให้บรรลุสิ่งที่ดีที่สุดของโลกทั้งสอง

หน่วยและการบูรณาการทดสอบความกลมกลืนในไฟล์เดียวกัน

การสร้างแฟล็กก่อนหน้านี้บังคับให้ฉันมีหลายไฟล์ ( services_test.go,services_integration_test.goฯลฯ )

ลองใช้ตัวอย่างนี้ด้านล่างโดยที่สองรายการแรกเป็นการทดสอบหน่วยและฉันมีการทดสอบการรวมในตอนท้าย:

package services

import "testing"

func TestServiceFunc(t *testing.T) {
    t.Parallel()
    ...
}

func TestInvalidServiceFunc3(t *testing.T) {
    t.Parallel()
    ...
}

func TestPostgresVersionIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
    ...
}

สังเกตว่าการทดสอบครั้งล่าสุดมีหลักการดังนี้

  1. โดยใช้ Integrationในชื่อการทดสอบ
  2. ตรวจสอบว่าทำงานภายใต้ -shortคำสั่งแฟล็กหรือไม่

โดยทั่วไปข้อมูลจำเพาะจะไป: "เขียนการทดสอบทั้งหมดตามปกติหากเป็นการทดสอบที่ใช้เวลานานหรือการทดสอบการรวมให้ทำตามหลักการตั้งชื่อนี้และตรวจสอบ -shortว่าเพื่อนของคุณดี"

รันเฉพาะการทดสอบหน่วย:

go test -v -short

สิ่งนี้จะให้ชุดข้อความที่ดีแก่คุณเช่น:

=== RUN   TestPostgresVersionIntegration
--- SKIP: TestPostgresVersionIntegration (0.00s)
        service_test.go:138: skipping integration test

รันการทดสอบการรวมเท่านั้น:

go test -run Integration

ซึ่งจะรันเฉพาะการทดสอบการรวมเท่านั้น มีประโยชน์สำหรับการทดสอบควันคีรีบูนในการผลิต

เห็นได้ชัดว่าข้อเสียของแนวทางนี้คือหากใครก็ตามที่ทำงานgo testโดยไม่มี-shortแฟล็กก็จะเรียกใช้การทดสอบทั้งหมด - การทดสอบหน่วยและการรวมโดยปริยาย

ในความเป็นจริงหากโปรเจ็กต์ของคุณมีขนาดใหญ่พอที่จะมีการทดสอบหน่วยและการบูรณาการคุณมักจะใช้Makefileที่ซึ่งคุณสามารถมีคำสั่งง่ายๆเพื่อใช้go test -shortในนั้นได้ หรือใส่ไว้ในREADME.mdไฟล์ของคุณแล้วเรียกมันว่าวัน


3
รักความเรียบง่าย
Jacob Stanley

คุณสร้างแพ็คเกจแยกต่างหากสำหรับการทดสอบดังกล่าวเพื่อเข้าถึงเฉพาะส่วนสาธารณะของแพ็คเกจหรือไม่? หรือผสมทั้งหมด?
Dr.eel

@ Dr.eel อืมที่ OT จากคำตอบ แต่โดยส่วนตัวแล้วฉันชอบทั้งสองอย่าง: ชื่อแพ็คเกจที่แตกต่างกันสำหรับการทดสอบดังนั้นฉันจึงสามารถimportแพ็กเกจของฉันและทดสอบกับมันได้ซึ่งท้ายที่สุดก็แสดงให้ฉันเห็นว่า API ของฉันเป็นอย่างไรสำหรับคนอื่น จากนั้นฉันจะติดตามด้วยตรรกะที่เหลือซึ่งจำเป็นต้องครอบคลุมเป็นชื่อแพ็คเกจการทดสอบภายใน
eduncan911

@ eduncan911 ขอบคุณสำหรับคำตอบ! ดังที่ฉันเข้าใจที่นี่package servicesมีการทดสอบการรวมดังนั้นในการทดสอบ APIfo แพคเกจเป็นกล่องดำเราควรตั้งชื่อมันอีกวิธีหนึ่งที่package services_integration_testจะไม่ทำให้เรามีโอกาสทำงานกับโครงสร้างภายใน ดังนั้นแพคเกจสำหรับการทดสอบหน่วย (internals เข้าถึง) package servicesควรตั้งชื่อ เป็นงั้นหรอ?
Dr.eel

ถูกต้องใช่ นี่คือตัวอย่างที่ชัดเจนของวิธีที่ฉันทำ: github.com/eduncan911/podcast (สังเกตการครอบคลุมรหัส 100% โดยใช้ตัวอย่าง)
eduncan911

51

ฉันเห็นสามวิธีที่เป็นไปได้ ประการแรกคือการใช้โหมดสั้นสำหรับการทดสอบหน่วย คุณจะใช้go test -shortกับการทดสอบหน่วยและแบบเดียวกัน แต่ไม่มีไฟล์-shortแฟล็กเพื่อเรียกใช้การทดสอบการรวมของคุณด้วย ไลบรารีมาตรฐานใช้โหมดสั้นเพื่อข้ามการทดสอบระยะยาวหรือทำให้การทดสอบทำงานเร็วขึ้นโดยการให้ข้อมูลที่ง่ายขึ้น

ประการที่สองคือการใช้แบบแผนและเรียกการทดสอบของคุณTestUnitFooหรือTestIntegrationFooจากนั้นใช้-runแฟล็กการทดสอบเพื่อแสดงว่าการทดสอบใดรัน ดังนั้นคุณจะใช้go test -run 'Unit'สำหรับการทดสอบหน่วยและgo test -run 'Integration'การทดสอบการรวม

os.Getenvตัวเลือกที่สามคือการใช้ตัวแปรสภาพแวดล้อมและได้รับมันในการตั้งค่าการทดสอบของคุณด้วย จากนั้นคุณจะใช้แบบง่ายgo testสำหรับการทดสอบหน่วยและFOO_TEST_INTEGRATION=true go testการทดสอบการรวม

โดยส่วนตัวแล้วฉันชอบ-shortวิธีแก้ปัญหาเนื่องจากง่ายกว่าและใช้ในไลบรารีมาตรฐานดังนั้นจึงดูเหมือนว่าเป็นวิธีแยก / ลดความซับซ้อนของการทดสอบที่ใช้เวลานานโดยพฤตินัย แต่โซลูชัน-runและos.Getenvโซลูชันมีความยืดหยุ่นมากกว่า (จำเป็นต้องใช้ความระมัดระวังมากขึ้นเช่นกันเนื่องจาก regexps เกี่ยวข้องด้วย-run)


1
โปรดทราบว่านักวิ่งทดสอบชุมชน (เช่นTester-Go) ทั่วไปของ IDEs (Atom, Sublime ฯลฯ ) มีตัวเลือกในตัวในการวิ่งด้วย-shortแฟล็กพร้อมด้วย-coverageและอื่น ๆ ดังนั้นฉันจึงใช้การรวมกันของทั้งสอง Integration ในชื่อการทดสอบพร้อมกับการif testing.Short()ตรวจสอบภายในการทดสอบเหล่านั้น มันช่วยให้ฉันมีสิ่งที่ดีที่สุดของทั้งสองโลก: วิ่งด้วย-shortภายใน IDE และเรียกใช้การทดสอบการรวมอย่างชัดเจนด้วยgo test -run "Integration"
eduncan911

6

ฉันพยายามหาวิธีแก้ปัญหาเดียวกันเมื่อไม่นานมานี้ นี่คือเกณฑ์ของฉัน:

  • การแก้ปัญหาต้องเป็นสากล
  • ไม่มีแพ็คเกจแยกต่างหากสำหรับการทดสอบการรวม
  • การแยกควรเสร็จสมบูรณ์ (ฉันควรจะสามารถเรียกใช้การทดสอบการรวมเท่านั้น )
  • ไม่มีรูปแบบการตั้งชื่อพิเศษสำหรับการทดสอบการรวม
  • ควรใช้งานได้ดีโดยไม่ต้องใช้เครื่องมือเพิ่มเติม

โซลูชันข้างต้น (แฟล็กที่กำหนดเองแท็กบิลด์ที่กำหนดเองตัวแปรสภาพแวดล้อม) ไม่ตรงตามเกณฑ์ข้างต้นทั้งหมดดังนั้นหลังจากการขุดและเล่นเพียงเล็กน้อยฉันก็ได้พบกับโซลูชันนี้:

package main

import (
    "flag"
    "regexp"
    "testing"
)

func TestIntegration(t *testing.T) {
    if m := flag.Lookup("test.run").Value.String(); m == "" || !regexp.MustCompile(m).MatchString(t.Name()) {
        t.Skip("skipping as execution was not requested explicitly using go test -run")
    }

    t.Parallel()

    t.Run("HelloWorld", testHelloWorld)
    t.Run("SayHello", testSayHello)
}

การนำไปใช้นั้นตรงไปตรงมาและน้อยที่สุด แม้ว่าจะต้องมีหลักการง่ายๆในการทดสอบ แต่ก็มีข้อผิดพลาดน้อยกว่า การปรับปรุงเพิ่มเติมอาจเป็นการส่งออกรหัสไปยังฟังก์ชันตัวช่วย

การใช้งาน

เรียกใช้การทดสอบการรวมเฉพาะในแพ็คเกจทั้งหมดในโปรเจ็กต์:

go test -v ./... -run ^TestIntegration$

เรียกใช้การทดสอบทั้งหมด ( ปกติและรวม):

go test -v ./... -run .\*

ทำการทดสอบตามปกติเท่านั้น:

go test -v ./...

โซลูชันนี้ทำงานได้ดีโดยไม่ต้องใช้เครื่องมือ แต่ Makefile หรือนามแฝงบางตัวสามารถทำให้ผู้ใช้ง่ายขึ้น นอกจากนี้ยังสามารถรวมเข้ากับ IDE ใด ๆ ที่รองรับการทดสอบการวิ่งได้อย่างง่ายดาย

สามารถดูตัวอย่างเต็มได้ที่นี่: https://github.com/sagikazarmark/modern-go-application

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.