ฉันมีความเข้าใจพื้นฐานของปลอมจำลองและวัตถุ แต่ผมไม่แน่ใจว่าผมมีความรู้สึกเกี่ยวกับเมื่อ / สถานที่ที่จะใช้เยาะเย้ย - โดยเฉพาะอย่างยิ่งมันจะนำไปใช้กับสถานการณ์นี้ที่นี่
คำตอบ:
การทดสอบหน่วยควรทดสอบ codepath เดียวด้วยวิธีการเดียว เมื่อการเรียกใช้เมธอดผ่านนอกเมธอดนั้นไปยังอ็อบเจ็กต์อื่นและกลับมาอีกครั้งคุณจะมีการพึ่งพา
เมื่อคุณทดสอบเส้นทางรหัสนั้นด้วยการอ้างอิงจริงคุณไม่ได้ทดสอบหน่วย คุณกำลังทดสอบการรวมระบบ แม้ว่าจะเป็นสิ่งที่ดีและจำเป็น แต่ก็ไม่ใช่การทดสอบหน่วย
หากการพึ่งพาของคุณมีข้อบกพร่องการทดสอบของคุณอาจได้รับผลกระทบในลักษณะที่จะส่งคืนผลบวกเท็จ ตัวอย่างเช่นคุณอาจส่งผ่านการอ้างอิงเป็นโมฆะที่ไม่คาดคิดและการอ้างอิงไม่สามารถทำให้เป็นโมฆะได้ตามที่บันทึกไว้ การทดสอบของคุณไม่มีข้อยกเว้นอาร์กิวเมนต์ว่างอย่างที่ควรจะเป็นและการทดสอบจะผ่านไป
นอกจากนี้คุณอาจพบว่ามันยากหากไม่เป็นไปไม่ได้ที่จะทำให้วัตถุอ้างอิงกลับมาตรงกับสิ่งที่คุณต้องการในระหว่างการทดสอบได้อย่างน่าเชื่อถือ นอกจากนี้ยังรวมถึงการทิ้งข้อยกเว้นที่คาดไว้ในการทดสอบ
การจำลองแทนที่การพึ่งพานั้น คุณตั้งความคาดหวังในการเรียกไปยังอ็อบเจ็กต์ที่เกี่ยวข้องตั้งค่าการส่งคืนที่แน่นอนที่ควรให้คุณทำการทดสอบที่คุณต้องการและ / หรือมีข้อยกเว้นอะไรบ้างที่จะทิ้งเพื่อให้คุณสามารถทดสอบโค้ดการจัดการข้อยกเว้นของคุณได้ ด้วยวิธีนี้คุณสามารถทดสอบหน่วยที่เป็นปัญหาได้อย่างง่ายดาย
TL; DR: จำลองการพึ่งพาการทดสอบหน่วยของคุณทุกครั้งที่สัมผัส
อ็อบเจ็กต์จำลองมีประโยชน์เมื่อคุณต้องการทดสอบการโต้ตอบระหว่างคลาสภายใต้การทดสอบและอินเทอร์เฟซเฉพาะ
ตัวอย่างเช่นเราต้องการทดสอบเมธอดนั้นsendInvitations(MailServer mailServer)
เรียกMailServer.createMessage()
เพียงครั้งเดียวและเรียกMailServer.sendMessage(m)
ครั้งเดียวเหมือนกันและจะไม่มีการเรียกวิธีอื่นบนMailServer
อินเทอร์เฟซ นี่คือเวลาที่เราสามารถใช้วัตถุจำลอง
ด้วยวัตถุจำลองแทนที่จะผ่านของจริงMailServerImpl
หรือการทดสอบTestMailServer
เราสามารถผ่านการจำลองการใช้งานMailServer
อินเทอร์เฟซได้ ก่อนที่เราจะผ่านการล้อเลียนMailServer
เราจะ "ฝึก" มันเพื่อให้รู้ว่าวิธีใดที่เรียกร้องให้คาดหวังและสิ่งที่จะคืนค่ากลับ ในตอนท้ายวัตถุจำลองยืนยันว่าเมธอดที่คาดหวังทั้งหมดถูกเรียกตามที่คาดไว้
ฟังดูดีในทางทฤษฎี แต่ก็มีข้อเสียบางประการเช่นกัน
หากคุณมีกรอบการทำงานจำลองคุณจะถูกล่อลวงให้ใช้วัตถุจำลองทุกครั้งที่คุณต้องส่งอินเทอร์เฟซไปยังคลาสภายใต้การทดสอบ วิธีนี้จะทำให้คุณทดสอบการโต้ตอบได้แม้ว่าจะไม่จำเป็นก็ตาม น่าเสียดายที่การทดสอบการโต้ตอบที่ไม่พึงประสงค์ (โดยไม่ได้ตั้งใจ) นั้นไม่ดีเนื่องจากคุณกำลังทดสอบว่าข้อกำหนดเฉพาะถูกนำไปใช้ในลักษณะเฉพาะแทนที่จะนำไปใช้งานนั้นจะให้ผลลัพธ์ที่ต้องการ
นี่คือตัวอย่างใน pseudocode สมมติว่าเราได้สร้างMySorter
คลาสขึ้นมาแล้วและเราต้องการทดสอบ:
// the correct way of testing
testSort() {
testList = [1, 7, 3, 8, 2]
MySorter.sort(testList)
assert testList equals [1, 2, 3, 7, 8]
}
// incorrect, testing implementation
testSort() {
testList = [1, 7, 3, 8, 2]
MySorter.sort(testList)
assert that compare(1, 2) was called once
assert that compare(1, 3) was not called
assert that compare(2, 3) was called once
....
}
(ในตัวอย่างนี้เราถือว่าไม่ใช่อัลกอริทึมการเรียงลำดับเฉพาะเช่นการเรียงลำดับด่วนที่เราต้องการทดสอบในกรณีนี้การทดสอบครั้งหลังจะใช้ได้จริง)
ในตัวอย่างที่รุนแรงเช่นนี้เห็นได้ชัดว่าทำไมตัวอย่างหลังจึงผิด เมื่อเราเปลี่ยนการใช้งานMySorter
การทดสอบครั้งแรกจะช่วยให้แน่ใจว่าเรายังคงเรียงลำดับอย่างถูกต้องซึ่งเป็นจุดรวมของการทดสอบ - ทำให้เราสามารถเปลี่ยนรหัสได้อย่างปลอดภัย ในทางกลับกันการทดสอบครั้งหลังมักจะหยุดพักและเป็นอันตรายอย่างมาก มันขัดขวางการปรับโครงสร้างใหม่
เฟรมเวิร์กจำลองมักจะอนุญาตให้ใช้งานที่เข้มงวดน้อยกว่าโดยที่เราไม่จำเป็นต้องระบุว่าควรเรียกวิธีการกี่ครั้งและพารามิเตอร์ใด พวกเขาอนุญาตให้สร้างวัตถุจำลองที่ใช้เป็นต้นขั้ว
สมมติว่าเรามีวิธีการsendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)
ที่เราต้องการทดสอบ PdfFormatter
วัตถุสามารถนำมาใช้ในการสร้างคำเชิญ นี่คือการทดสอบ:
testInvitations() {
// train as stub
pdfFormatter = create mock of PdfFormatter
let pdfFormatter.getCanvasWidth() returns 100
let pdfFormatter.getCanvasHeight() returns 300
let pdfFormatter.addText(x, y, text) returns true
let pdfFormatter.drawLine(line) does nothing
// train as mock
mailServer = create mock of MailServer
expect mailServer.sendMail() called exactly once
// do the test
sendInvitations(pdfFormatter, mailServer)
assert that all pdfFormatter expectations are met
assert that all mailServer expectations are met
}
ในตัวอย่างนี้เราไม่สนใจPdfFormatter
วัตถุจริงๆดังนั้นเราแค่ฝึกให้มันรับสายอย่างเงียบ ๆ และส่งคืนค่าผลตอบแทนกระป๋องที่สมเหตุสมผลสำหรับวิธีการทั้งหมดที่sendInvitation()
เกิดขึ้นกับการเรียก ณ จุดนี้ เราคิดรายการวิธีการฝึกอบรมนี้ได้อย่างไร? เราทำการทดสอบและเพิ่มวิธีการต่อไปจนกว่าการทดสอบจะผ่าน สังเกตว่าเราได้ฝึกฝนต้นขั้วให้ตอบสนองต่อวิธีการโดยไม่ทราบสาเหตุว่าทำไมจึงต้องเรียกมันเราเพียงแค่เพิ่มทุกสิ่งที่การทดสอบบ่น เรามีความสุขการทดสอบผ่านไป
แต่จะเกิดอะไรขึ้นในภายหลังเมื่อเราเปลี่ยนsendInvitations()
หรือคลาสอื่น ๆ ที่sendInvitations()
ใช้เพื่อสร้างไฟล์ PDF ที่สวยงามมากขึ้น? การทดสอบของเราล้มเหลวอย่างกะทันหันเพราะตอนนี้มีการPdfFormatter
เรียกวิธีการมากขึ้นและเราไม่ได้ฝึกต้นขั้วให้คาดหวัง และโดยปกติแล้วไม่ใช่แค่การทดสอบเดียวที่ล้มเหลวในสถานการณ์เช่นนี้ แต่เป็นการทดสอบใด ๆ ที่เกิดขึ้นกับการใช้sendInvitations()
วิธีนี้ไม่ว่าทางตรงหรือทางอ้อม เราต้องแก้ไขการทดสอบเหล่านั้นทั้งหมดโดยเพิ่มการฝึกอบรมให้มากขึ้น โปรดสังเกตด้วยว่าเราไม่สามารถลบวิธีการที่ไม่จำเป็นได้อีกต่อไปเพราะเราไม่รู้ว่าวิธีใดที่ไม่จำเป็น อีกครั้งเป็นอุปสรรคต่อการปรับโครงสร้างใหม่
นอกจากนี้ความสามารถในการอ่านของการทดสอบได้รับความเดือดร้อนอย่างมากมีโค้ดมากมายที่เราไม่ได้เขียนเพราะเราต้องการ แต่เพราะเราต้องทำ ไม่ใช่พวกเราที่ต้องการรหัสนั้น การทดสอบที่ใช้วัตถุจำลองดูซับซ้อนมากและมักจะอ่านยาก การทดสอบควรช่วยให้ผู้อ่านเข้าใจว่าควรใช้ชั้นเรียนภายใต้การทดสอบอย่างไรจึงควรง่ายและตรงไปตรงมา ถ้าอ่านไม่ออกก็ไม่มีใครดูแลมัน ในความเป็นจริงการลบมันง่ายกว่าการดูแลรักษา
จะแก้ไขได้อย่างไร? ได้อย่างง่ายดาย:
PdfFormatterImpl
. หากไม่สามารถทำได้ให้เปลี่ยนคลาสจริงเพื่อให้เป็นไปได้ การไม่สามารถใช้ชั้นเรียนในการทดสอบมักชี้ให้เห็นปัญหาบางอย่างกับชั้นเรียน การแก้ไขปัญหาเป็นสถานการณ์ที่ชนะ - คุณแก้ไขคลาสแล้วและคุณมีการทดสอบที่ง่ายกว่า ในทางกลับกันการไม่แก้ไขและการใช้ล้อเลียนเป็นสถานการณ์ที่ไม่มีทางชนะคุณไม่ได้แก้ไขคลาสจริงและคุณมีการทดสอบที่ซับซ้อนและอ่านได้น้อยกว่าซึ่งขัดขวางการปรับแต่งเพิ่มเติมTestPdfFormatter
ที่ไม่ทำอะไรเลย ด้วยวิธีนี้คุณสามารถเปลี่ยนได้เพียงครั้งเดียวสำหรับการทดสอบทั้งหมดและการทดสอบของคุณจะไม่เกะกะกับการตั้งค่าที่ยาวนานซึ่งคุณจะต้องฝึกต้นขั้วของคุณทั้งหมดในทุกวัตถุจำลองมีการใช้งานของพวกเขา แต่เมื่อไม่ได้ใช้อย่างระมัดระวังพวกเขามักจะส่งเสริมให้การปฏิบัติที่ไม่ดีการทดสอบรายละเอียดการดำเนินขัดขวาง refactoring และผลิตยากที่จะอ่านและยากที่จะรักษาทดสอบ
สำหรับรายละเอียดบางอย่างเพิ่มเติมเกี่ยวกับข้อบกพร่องของ mocks เห็นวัตถุจำลอง: บกพร่องและกรณีการใช้งาน
หลักการง่ายๆ:
หากฟังก์ชันที่คุณกำลังทดสอบต้องการอ็อบเจ็กต์ที่ซับซ้อนเป็นพารามิเตอร์และการสร้างอินสแตนซ์ของอ็อบเจ็กต์นี้จะเป็นเรื่องยาก (เช่นหากพยายามสร้างการเชื่อมต่อ TCP) ให้ใช้การจำลอง
คุณควรเยาะเย้ยวัตถุเมื่อคุณมีการพึ่งพาในหน่วยของรหัสที่คุณพยายามทดสอบว่าต้องเป็น "อย่างนั้น"
ตัวอย่างเช่นเมื่อคุณพยายามทดสอบตรรกะบางอย่างในหน่วยรหัสของคุณ แต่คุณจำเป็นต้องได้รับบางสิ่งจากวัตถุอื่นและสิ่งที่ส่งกลับมาจากการพึ่งพานี้อาจส่งผลต่อสิ่งที่คุณพยายามทดสอบ - ล้อเลียนวัตถุนั้น
คุณสามารถดูพอดคาสต์ที่ยอดเยี่ยมในหัวข้อนี้ได้ที่นี่