วิธีการทดสอบหน่วยควรจะเขียนโดยไม่เยาะเย้ยอย่างกว้างขวาง?


80

ตามที่ผมเข้าใจจุดของการทดสอบหน่วยคือการทดสอบหน่วยของรหัสในการแยก หมายความว่า:

  1. พวกเขาไม่ควรทำลายโดยการเปลี่ยนแปลงรหัสที่ไม่เกี่ยวข้องใดๆ ใน codebase
  2. การทดสอบหน่วยเดียวควรทำลายข้อบกพร่องในหน่วยการทดสอบซึ่งตรงข้ามกับการทดสอบการรวม (ซึ่งอาจแบ่งเป็นฮีป)

ทั้งหมดนี้แสดงถึงว่าควรมีการเยาะเย้ยจากภายนอกหน่วยทดสอบ และฉันหมายถึงการพึ่งพาภายนอกทั้งหมดไม่เพียง แต่ "เลเยอร์นอก" เช่นระบบเครือข่ายระบบไฟล์ฐานข้อมูล ฯลฯ

นี้นำไปสู่ข้อสรุปเชิงตรรกะที่แทบทุกหน่วยทดสอบความต้องการที่จะเยาะเย้ย ในทางกลับกันการค้นหาอย่างรวดเร็วของ Google เกี่ยวกับการเยาะเย้ยเผยให้เห็นบทความมากมายที่อ้างว่า "การเยาะเย้ยเป็นกลิ่นรหัส" และส่วนใหญ่ควรหลีกเลี่ยง (แม้ว่าจะไม่สมบูรณ์)

ตอนนี้สำหรับคำถาม

  1. การทดสอบหน่วยควรเขียนอย่างถูกต้องอย่างไร?
  2. เส้นแบ่งระหว่างพวกเขากับการทดสอบการรวมอยู่ตรงไหน?

อัปเดต 1

โปรดพิจารณารหัสหลอกต่อไปนี้:

class Person {
    constructor(calculator) {}

    calculate(a, b) {
        const sum = this.calculator.add(a, b);

        // do some other stuff with the `sum`
    }
}

การทดสอบที่ทดสอบPerson.calculateวิธีการโดยไม่มีการเยาะเย้ยการCalculatorพึ่งพา (ที่ระบุว่าCalculatorคลาสที่มีน้ำหนักเบาที่ไม่สามารถเข้าถึง "โลกภายนอก") ถือเป็นการทดสอบหน่วยได้หรือไม่?


10
ส่วนนี้เป็นเพียงประสบการณ์การออกแบบที่จะมาพร้อมกับเวลา คุณจะได้เรียนรู้วิธีการจัดโครงสร้างส่วนประกอบของคุณเพื่อที่พวกเขาจะได้ไม่ต้องพึ่งพาการเยาะเย้ย ซึ่งหมายความว่าการทดสอบต้องเป็นเป้าหมายการออกแบบรองของซอฟต์แวร์ใด ๆ เป้าหมายนี้ง่ายกว่ามากหากการทดสอบถูกเขียนก่อนหรือร่วมกับรหัสเช่นใช้ TDD และ / หรือ BDD
amon

33
ใส่การทดสอบที่ทำงานได้อย่างรวดเร็วและเชื่อถือได้ในโฟลเดอร์เดียว ใส่การทดสอบที่ช้าและอาจเปราะบางเข้าด้วยกัน เรียกใช้การทดสอบในโฟลเดอร์แรกบ่อยที่สุดเท่าที่จะทำได้ (แท้จริงทุกครั้งที่คุณหยุดพิมพ์และการคอมไพล์โค้ดนั้นเป็นอุดมคติ แต่ไม่ใช่ทุกสภาพแวดล้อม dev สนับสนุนสิ่งนี้) เรียกใช้การทดสอบช้าลงน้อยกว่าบ่อยครั้ง (เมื่อคุณมีช่วงพักดื่มกาแฟ) ไม่ต้องกังวลเกี่ยวกับชื่อหน่วยและการรวม เรียกพวกเขาอย่างรวดเร็วและช้าถ้าคุณต้องการ มันไม่สำคัญ
David Arno

6
"การทดสอบทุกหน่วยต้องมีการเยาะเย้ย" ใช่แล้ว "การค้นหาของ Google อย่างรวดเร็วเกี่ยวกับเยาะเย้ยเผยตันของบทความที่อ้างว่า 'เยาะเย้ยเป็นกลิ่นรหัส'" พวกเขากำลังที่ไม่ถูกต้อง
Michael

13
@Michael เพียงแค่พูดว่า 'ใช่แล้ว' และการประกาศมุมมองฝ่ายตรงข้ามผิดก็ไม่ใช่วิธีที่ดีในการเข้าถึงเรื่องที่ถกเถียงเช่นนี้ บางทีเขียนคำตอบและอธิบายอย่างละเอียดถึงสาเหตุที่คุณคิดว่าการเยาะเย้ยควรอยู่ทุกหนทุกแห่งและบางทีทำไมคุณคิดว่า 'บทความมากมาย' ผิดไปโดยธรรมชาติ?
AJFaraday

6
เนื่องจากคุณไม่ได้ให้การอ้างอิงสำหรับ "การเยาะเย้ยเป็นกลิ่นรหัส" ฉันสามารถเดาได้ว่าคุณตีความสิ่งที่คุณอ่านผิดไปเท่านั้น การเยาะเย้ยไม่ใช่กลิ่นรหัส จำเป็นต้องใช้การสะท้อนหรือ shenanigans อื่น ๆ เพื่อฉีด mocks ของคุณเป็นกลิ่นรหัส ความยากลำบากในการเยาะเย้ยนั้นแปรผกผันกับคุณภาพของการออกแบบ API ของคุณ หากคุณสามารถเขียนการทดสอบหน่วยที่ตรงไปตรงมาง่ายๆที่เพิ่งผ่าน mocks ไปยังตัวสร้างคุณกำลังทำถูกต้อง
TKK

คำตอบ:


59

จุดทดสอบหน่วยคือการทดสอบหน่วยของรหัสในการแยก

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

การทดสอบหน่วยมักถูกพูดถึงในการพัฒนาซอฟต์แวร์และเป็นคำศัพท์ที่ฉันคุ้นเคยกับโปรแกรมการเขียนตลอดเวลา เช่นเดียวกับคำศัพท์ในการพัฒนาซอฟต์แวร์ส่วนใหญ่ แต่ก็มีคำจำกัดความที่ไม่ดีและฉันเห็นความสับสนเกิดขึ้นบ่อยครั้งเมื่อผู้คนคิดว่ามันถูกนิยามไว้แน่นกว่าที่เป็นจริง

สิ่งที่ Kent Beck เขียนในการพัฒนาแบบทดสอบขับเคลื่อนตามตัวอย่าง

ฉันเรียกพวกเขาว่า "การทดสอบหน่วย" แต่พวกเขาไม่ตรงกับคำจำกัดความที่ยอมรับได้ของการทดสอบหน่วยดีมาก

การอ้างสิทธิ์ใด ๆ ที่ระบุว่า "ประเด็นของการทดสอบหน่วยคือ" จะขึ้นอยู่กับคำจำกัดความของ "การทดสอบหน่วย" อย่างมาก

ถ้ามุมมองของคุณคือการที่โปรแกรมของคุณประกอบด้วยหน่วยขนาดเล็กจำนวนมากที่ขึ้นอยู่กับอีกคนหนึ่งและถ้าคุณ จำกัด ตัวเองให้เข้ากับสไตล์ที่ทดสอบแต่ละหน่วยในการแยกแล้วจำนวนมากของการทดสอบคู่เป็นข้อสรุปแน่นอน

คำแนะนำที่ขัดแย้งกันที่คุณเห็นมาจากคนที่ดำเนินงานภายใต้สมมติฐานที่ต่างกัน

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

คุณอาจต้องการเปรียบเทียบ:

การทดสอบที่ทดสอบวิธี Person.calculate สามารถทำได้โดยไม่ต้องจำลองการอ้างอิงของเครื่องคิดเลข (ระบุว่าเครื่องคิดเลขเป็นคลาสที่มีน้ำหนักเบาและไม่สามารถเข้าถึง "โลกภายนอก") ได้หรือไม่?

ฉันคิดว่านั่นเป็นคำถามที่ผิดที่จะถาม; มันเป็นอีกข้อโต้แย้งเกี่ยวกับฉลากเมื่อผมเชื่อว่าสิ่งที่เราดูแลจริงเกี่ยวกับคุณสมบัติ

เมื่อฉันแนะนำการเปลี่ยนแปลงรหัสฉันไม่สนใจเกี่ยวกับการแยกการทดสอบ - ฉันรู้แล้วว่า "ความผิดพลาด" นั้นอยู่ที่อื่นในการแก้ไขที่ไม่ได้ตรวจสอบ ถ้าฉันทำการทดสอบบ่อยๆฉันก็จำกัดความลึกของสแต็กนั้นและการค้นหาความผิดพลาดนั้นเป็นเรื่องเล็กน้อย (ในกรณีที่รุนแรงการทดสอบจะดำเนินการหลังจากแก้ไขทุกครั้ง - ความลึกสูงสุดของสแต็กคือหนึ่ง) แต่การรันการทดสอบไม่ใช่เป้าหมาย แต่เป็นการหยุดชะงัก - ดังนั้นจึงมีคุณค่าในการลดผลกระทบของการหยุดชะงัก วิธีหนึ่งในการลดการหยุดชะงักคือเพื่อให้แน่ใจว่าการทดสอบนั้นรวดเร็ว ( Gary Bernhardt แนะนำ 300 มิลลิวินาทีแต่ฉันไม่ได้หาวิธีที่จะทำในสถานการณ์ของฉัน)

หากการเรียกใช้Calculator::addไม่เพิ่มเวลาที่จำเป็นในการเรียกใช้การทดสอบ (หรือคุณสมบัติที่สำคัญอื่น ๆ สำหรับกรณีการใช้งานนี้) อย่างมีนัยสำคัญแล้วฉันจะไม่รำคาญกับการทดสอบซ้ำ - ไม่ให้ประโยชน์ที่เกินดุลค่าใช้จ่าย .

สังเกตสมมติฐานสองข้อที่นี่: มนุษย์เป็นส่วนหนึ่งของการประเมินค่าใช้จ่ายและสแต็คสั้น ๆ ของการเปลี่ยนแปลงที่ไม่ได้ตรวจสอบในการประเมินผลประโยชน์ ในสถานการณ์ที่ไม่มีเงื่อนไขเหล่านั้นค่าของ "การแยก" จะเปลี่ยนไปเล็กน้อย

ดูHot Lavaโดย Harry Percival


5
สิ่งหนึ่งที่เยาะเย้ยเพิ่มไม่ทำคือการพิสูจน์เครื่องคิดเลขที่สามารถนำมาล้อเลียนเช่นว่าการออกแบบไม่ได้คนทั้งคู่และเครื่องคิดเลข (แม้ว่านี้ยังสามารถตรวจสอบได้ในรูปแบบอื่น ๆ )
JK

40

วิธีการทดสอบหน่วยควรจะเขียนโดยไม่เยาะเย้ยอย่างกว้างขวาง?

โดยลดผลข้างเคียงในรหัสของคุณ

จดรหัสตัวอย่างของคุณถ้าcalculatorพูดถึงเว็บ API คุณก็จะสร้างการทดสอบที่เปราะบางซึ่งต้องอาศัยความสามารถในการโต้ตอบกับเว็บ API นั้นหรือสร้างการจำลองขึ้นมา อย่างไรก็ตามหากเป็นฟังก์ชันการคำนวณที่กำหนดค่าได้อย่างอิสระและไร้ค่าคุณก็จะไม่เยาะเย้ย (และไม่ควร) หากคุณทำเช่นนั้นคุณเสี่ยงต่อการเยาะเย้ยของคุณที่ต่างไปจากรหัสจริงซึ่งนำไปสู่ข้อบกพร่องในการทดสอบของคุณ

Mocks ควรจำเป็นสำหรับรหัสที่อ่าน / เขียนไปยังระบบไฟล์ฐานข้อมูลปลายทาง URL ฯลฯ ที่ขึ้นอยู่กับสภาพแวดล้อมที่คุณใช้งาน หรือที่รัฐสูงและไม่กำหนดในธรรมชาติ ดังนั้นหากคุณเก็บส่วนของรหัสเหล่านั้นให้อยู่ในระดับต่ำสุดและซ่อนไว้เบื้องหลัง abstractions แล้วพวกเขาจะเยาะเย้ยได้ง่ายและส่วนที่เหลือของรหัสของคุณจะหลีกเลี่ยงความต้องการ mocks

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


8
นี่เป็นคำตอบที่ถูกต้องทั้งในทางปฏิบัติและในแง่ของการหลบหลีกการอภิปรายเชิงความหมายที่ไม่มีความหมาย
Jared Smith

คุณมีตัวอย่างของ codebase โอเพ่นซอร์สที่ไม่ได้ใช้เรื่องสไตล์และยังคงได้รับการทดสอบที่ดีหรือไม่?
Joeri Sebrechts

4
@JoeriSebrechts ทุกหนึ่ง FP หรือไม่ ตัวอย่าง
Jared Smith

ไม่ใช่สิ่งที่ฉันกำลังมองหาเพราะมันเป็นเพียงชุดของฟังก์ชั่นที่เป็นอิสระจากกันไม่ใช่ระบบของฟังก์ชั่นที่เรียกกัน คุณจะต้องสร้างอาร์กิวเมนต์ที่ซับซ้อนให้กับฟังก์ชันเพื่อทดสอบได้อย่างไรถ้าฟังก์ชันนั้นเป็นหนึ่งในระดับบนสุด เช่นวงหลักของเกม
Joeri Sebrechts

1
@JoeriSebrechts อืมฉันอาจเข้าใจผิดว่าเธอต้องการอะไรหรือเธอไม่ได้ขุดลึกลงไปในตัวอย่างที่ฉันให้มา ฟังก์ชั่น ramda ใช้การโทรภายในทั่วทุกสถานที่ในแหล่งที่มา (เช่นR.equals) เนื่องจากสิ่งเหล่านี้มีไว้สำหรับฟังก์ชั่นบริสุทธิ์ส่วนใหญ่พวกเขามักจะไม่เยาะเย้ยในการทดสอบ
Jared Smith

31

คำถามเหล่านี้มีความแตกต่างค่อนข้างยาก ลองทำคำถามที่ 2 ก่อน

การทดสอบหน่วยและการทดสอบการรวมจะแยกกันอย่างชัดเจน การทดสอบหน่วยทดสอบหนึ่งหน่วย (วิธีการหรือคลาส) และใช้หน่วยอื่น ๆ เท่าที่จำเป็นเพื่อให้บรรลุเป้าหมายนั้น การเยาะเย้ยอาจจำเป็น แต่มันไม่ได้เป็นจุดของการทดสอบ การทดสอบการรวมระบบจะทดสอบการทำงานร่วมกันระหว่างหน่วยงานจริงที่แตกต่างกัน ความแตกต่างนี้เป็นเหตุผลทั้งหมดว่าทำไมเราจึงต้องการการทดสอบทั้งหน่วยและการรวมเข้าด้วยกัน - หากเราทำหน้าที่ของอีกฝ่ายได้ดีพอเราจะไม่ทำ แต่มันกลับกลายเป็นว่าปกติแล้วมันจะมีประสิทธิภาพมากกว่าในการใช้เครื่องมือพิเศษสองอย่าง .

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

ผู้ปฏิบัติงานหลายคนกลัวว่าถ้าการทดสอบหน่วย B นำคลาสที่ผ่านการทดสอบแล้วมาใช้อีกต่อไปโดยการทดสอบยูนิท A ข้อบกพร่องในยูนิต A ทำให้การทดสอบล้มเหลวในหลาย ๆ ที่ ผมคิดว่าเรื่องนี้ไม่ได้เป็นปัญหา: ชุดทดสอบมีการประสบความสำเร็จ100%ในการสั่งซื้อเพื่อให้คุณมีความมั่นใจที่คุณต้องการดังนั้นจึงไม่เป็นปัญหาใหญ่ที่จะมีความล้มเหลวมากเกินไป - หลังจากที่ทุกท่านทำมีข้อบกพร่อง ปัญหาที่สำคัญเพียงอย่างเดียวคือหากมีข้อบกพร่องเกิดขึ้นกับความล้มเหลวน้อยเกินไป

ดังนั้นอย่าสร้างศาสนาแห่งการเยาะเย้ย มันเป็นวิธีการไม่สิ้นสุดดังนั้นหากคุณสามารถหลีกเลี่ยงความพยายามพิเศษได้คุณควรทำเช่นนั้น


4
The only critical problem would be if a defect triggered too few failures.นี่เป็นหนึ่งในจุดอ่อนของการเยาะเย้ย เราต้อง "โปรแกรม" พฤติกรรมที่คาดหวังดังนั้นเราอาจล้มเหลวในการทำเช่นนั้นทำให้การทดสอบของเราสิ้นสุดในฐานะ "ผลบวกผิด" แต่การเยาะเย้ยเป็นเทคนิคที่มีประโยชน์มากเพื่อให้บรรลุถึงระดับ (เงื่อนไขที่สำคัญที่สุดของการทดสอบ) ฉันใช้มันในทุกโครงการเมื่อเป็นไปได้ พวกเขายังแสดงให้ฉันเห็นว่าการรวมกันนั้นซับซ้อนเกินไปหรือพึ่งพายากเกินไป
Laiv

1
หากหน่วยที่กำลังทดสอบใช้หน่วยอื่น ๆ นั่นเป็นการทดสอบการรวมระบบจริง ๆ หรือไม่ เพราะสาระสำคัญหน่วยนี้จะทำการทดสอบการมีปฏิสัมพันธ์ระหว่างหน่วยงานดังกล่าวเหมือนกับการทดสอบการรวมกลุ่ม
Alexander Lomia

11
@AlexanderLomia: คุณจะเรียกหน่วยอะไร คุณจะเรียก 'String' เป็นหน่วยเช่นกันหรือไม่ ฉันจะ แต่ฉันจะไม่ฝันที่จะเยาะเย้ยมัน
Bart van Ingen Schenau

5
" การทดสอบหน่วยและการทดสอบการรวมเข้าด้วยกันจะแยกกันอย่างชัดเจนการทดสอบหน่วยทดสอบหนึ่งหน่วย (วิธีหรือคลาส) และใช้หน่วยอื่น ๆ เท่าที่จำเป็นเพื่อให้บรรลุเป้าหมายนั้น " นี่คือการถู นั่นคือคุณความหมายของการทดสอบหน่วย ของฉันค่อนข้างแตกต่าง ดังนั้นความแตกต่างระหว่างพวกเขาจึงเป็น "แยกกันอย่างชัดเจน" สำหรับคำจำกัดความที่กำหนด แต่การแยกนั้นแตกต่างกันระหว่างคำจำกัดความ
David Arno

4
@Voo ต้องทำงานกับ codebases ดังกล่าวในขณะที่มันอาจสร้างความรำคาญให้กับการค้นหาปัญหาดั้งเดิม (โดยเฉพาะอย่างยิ่งถ้าสถาปัตยกรรมได้ทาสีสิ่งที่คุณใช้ในการดีบักมัน) ฉันยังคงมีปัญหาจาก mocks ที่ทำให้ การทดสอบเพื่อให้ทำงานหลังจากรหัสที่พวกเขาใช้ในการทดสอบได้แตก
James_pic

6

ตกลงเพื่อตอบคำถามของคุณโดยตรง:

การทดสอบหน่วยควรเขียนอย่างถูกต้องอย่างไร?

อย่างที่คุณพูดคุณควรจะล้อเลียนการพึ่งพาและทดสอบหน่วยที่เป็นปัญหา

เส้นแบ่งระหว่างพวกเขากับการทดสอบการรวมอยู่ตรงไหน?

การทดสอบการรวมเป็นการทดสอบหน่วยที่การอ้างอิงของคุณไม่ได้ถูกเยาะเย้ย

การทดสอบที่ทดสอบวิธี Person.calculate โดยไม่ต้องมีการเยาะเย้ยเครื่องคิดเลขถือเป็นการทดสอบหน่วยได้หรือไม่?

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

อย่างไรก็ตามข้อแม้ คุณสนใจสิ่งที่ผู้คนคิดว่าควรจะทำการทดสอบของคุณหรือไม่?

แต่คำถามที่แท้จริงของคุณน่าจะเป็นเช่นนี้:

การค้นหาอย่างรวดเร็วของ Google เกี่ยวกับการเยาะเย้ยเผยให้เห็นบทความมากมายที่อ้างว่า "การเยาะเย้ยเป็นกลิ่นรหัส" และส่วนใหญ่ควรหลีกเลี่ยง (แม้ว่าจะไม่สมบูรณ์)

ฉันคิดว่าปัญหาที่นี่คือผู้คนจำนวนมากใช้ mocks เพื่อสร้างการอ้างอิงที่สมบูรณ์ ตัวอย่างเช่นฉันอาจจำลองเครื่องคิดเลขในตัวอย่างของคุณเป็น

public class MockCalc : ICalculator
{
     public Add(int a, int b) { return 4; }
}

ฉันจะไม่ทำสิ่งที่ชอบ:

myMock = Mock<ICalculator>().Add((a,b) => {return a + b;})
myPerson.Calculate()
Assert.WasCalled(myMock.Add());

ฉันจะยืนยันว่านั่นคือ "การทดสอบการเยาะเย้ย" หรือ "การทดสอบการใช้งาน" ฉันจะพูดว่า " อย่าเขียน Mocks! * อย่างนั้น"

คนอื่นจะไม่เห็นด้วยกับฉันเราจะเริ่มสงครามเปลวไฟขนาดใหญ่บนบล็อกของเราเกี่ยวกับวิธีที่ดีที่สุดเพื่อเยาะเย้ยซึ่งจะทำให้รู้สึกไม่จริงเว้นแต่คุณจะเข้าใจพื้นหลังทั้งหมดของวิธีการต่างๆและไม่ได้ให้คุณค่ามากมาย ถึงคนที่เพียงแค่ต้องการที่จะเขียนการทดสอบที่ดี


ขอบคุณสำหรับคำตอบที่ครบถ้วนสมบูรณ์ จากความใส่ใจในสิ่งที่คนอื่นคิดเกี่ยวกับการทดสอบของฉัน - จริง ๆ แล้วฉันต้องการหลีกเลี่ยงการเขียนการทดสอบแบบกึ่งบูรณาการการทดสอบกึ่งหน่วย
Alexander Lomia

ไม่มีปัญหาฉันคิดว่าปัญหาคือคำจำกัดความของทั้งสองสิ่งไม่ได้ตกลงกันโดย 100%
Ewan

ฉันจะเปลี่ยนชื่อชั้นเรียนของคุณMockCalcเป็นStubCalcและเรียกมันว่าต้นขั้วไม่เยาะเย้ย martinfowler.com/articles/…
bdsl

@bdsl บทความนี้มีอายุ 15 ปี
Ewan

4
  1. การทดสอบหน่วยควรใช้งานอย่างถูกต้องอย่างไร?

กฎง่ายๆคือการทดสอบหน่วยที่เหมาะสม:

  • จะเขียนกับอินเตอร์เฟซที่ไม่ได้ใช้งาน สิ่งนี้มีประโยชน์มากมาย สำหรับหนึ่งก็เพื่อให้แน่ใจว่าชั้นเรียนของคุณตามพึ่งพาผกผันหลักการจากมูลฝอย นอกจากนี้นี่คือสิ่งที่คลาสอื่น ๆ ของคุณทำ ( ใช่ไหม ) ดังนั้นการทดสอบของคุณควรทำแบบเดียวกัน นอกจากนี้ยังช่วยให้คุณสามารถทดสอบการใช้งานหลาย ๆ อินเทอร์เฟซเดียวกันในขณะที่นำรหัสการทดสอบจำนวนมากมาใช้ใหม่ (เฉพาะการเริ่มต้นและการยืนยันบางอย่างจะเปลี่ยนไป)
  • มีอยู่ในตัวเอง ดังที่คุณกล่าวการเปลี่ยนแปลงในโค้ดภายนอกใด ๆ จะไม่ส่งผลต่อผลการทดสอบ ดังนั้นการทดสอบหน่วยสามารถดำเนินการในเวลาบิลด์อิน ซึ่งหมายความว่าคุณต้องการ mocks เพื่อลบผลข้างเคียงใด ๆ อย่างไรก็ตามหากคุณปฏิบัติตามหลักการผกผันของการพึ่งพาซึ่งควรจะค่อนข้างง่าย เฟรมเวิร์กการทดสอบที่ดีเช่นSpockสามารถใช้เพื่อจัดเตรียมการปรับใช้ mock ของอินเตอร์เฟสใด ๆ เพื่อใช้ในการทดสอบของคุณด้วยการเข้ารหัสน้อยที่สุด ซึ่งหมายความว่าแต่ละคลาสการทดสอบจำเป็นต้องใช้รหัสจากคลาสการใช้งานหนึ่งคลาสเท่านั้นรวมถึงเฟรมเวิร์กการทดสอบ (และอาจเป็นคลาสโมเดล ["beans"])
  • ไม่จำเป็นต้องมีการประยุกต์ใช้การทำงานที่แยกจากกัน หากการทดสอบจำเป็นต้อง "พูดคุยกับบางสิ่ง" ไม่ว่าจะเป็นฐานข้อมูลหรือบริการบนเว็บนั่นเป็นการทดสอบการรวมระบบไม่ใช่การทดสอบหน่วย ฉันวาดเส้นที่การเชื่อมต่อเครือข่ายหรือระบบไฟล์ ตัวอย่างเช่นฐานข้อมูล SQLite ในหน่วยความจำล้วนๆเป็นเกมที่ยุติธรรมในความคิดของฉันสำหรับการทดสอบหน่วยหากคุณต้องการจริงๆ

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

  1. เส้นแบ่งระหว่าง [การทดสอบหน่วย] และการทดสอบการรวมอยู่ตรงไหน?

ฉันพบความแตกต่างนี้มีประโยชน์มากที่สุด:

  • การทดสอบหน่วยจำลอง "รหัสผู้ใช้"พฤติกรรมของการเรียนการดำเนินการตรวจสอบกับพฤติกรรมที่ต้องการและความหมายของการเชื่อมต่อรหัสระดับ
  • การทดสอบบูรณาการจำลองผู้ใช้ตรวจสอบพฤติกรรมของแอปพลิเคชันที่ทำงานกับกรณีการใช้งานที่ระบุและ / หรือ API ที่เป็นทางการ สำหรับบริการบนเว็บ "ผู้ใช้" จะเป็นแอปพลิเคชันไคลเอนต์

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

  1. เป็นความจริงหรือเปล่าที่การทดสอบทุกหน่วยต้องมีการล้อเลียน?

ไม่กรณีทดสอบบางอย่างจะมีเงื่อนไขข้อผิดพลาดเช่นผ่านnullเป็นพารามิเตอร์และตรวจสอบว่าคุณได้รับข้อยกเว้น การทดสอบจำนวนมากเช่นนั้นจะไม่ต้องใช้ mocks ใด ๆ นอกจากนี้การใช้งานที่ไม่มีผลข้างเคียงเช่นการประมวลผลสตริงหรือฟังก์ชันคณิตศาสตร์อาจไม่ต้องการ mocks ใด ๆ เพราะคุณเพียงแค่ตรวจสอบผลลัพธ์ แต่ชั้นเรียนส่วนใหญ่มีค่าฉันคิดว่าจะต้องจำลองอย่างน้อยหนึ่งที่ในรหัสทดสอบ (ยิ่งน้อยยิ่งดี)

ปัญหา "code ดมกลิ่น" ที่คุณพูดถึงเกิดขึ้นเมื่อคุณมีคลาสที่ซับซ้อนเกินไปซึ่งต้องใช้รายการอ้างอิงแบบจำลองขนาดยาวเพื่อเขียนการทดสอบของคุณ นี่เป็นเงื่อนงำที่คุณจำเป็นต้องปรับโครงสร้างการนำไปใช้ใหม่และแยกสิ่งต่าง ๆ ออกมาเพื่อให้แต่ละชั้นเรียนมีขนาดเล็กกว่าและมีความรับผิดชอบที่ชัดเจนยิ่งขึ้นดังนั้นจึงสามารถทดสอบได้ง่ายขึ้น สิ่งนี้จะปรับปรุงคุณภาพในระยะยาว

การทดสอบหน่วยเดียวเท่านั้นที่ควรทำลายด้วยจุดบกพร่องในหน่วยการทดสอบ

ฉันไม่คิดว่านี่เป็นความคาดหวังที่สมเหตุสมผลเพราะใช้กับการใช้ซ้ำได้ privateตัวอย่างเช่นคุณอาจมีวิธีการที่ถูกเรียกใช้โดยหลายpublicวิธีที่เผยแพร่โดยส่วนต่อประสานของคุณ ข้อผิดพลาดที่แนะนำในวิธีหนึ่งนั้นอาจทำให้เกิดการทดสอบล้มเหลวหลายครั้ง นี่ไม่ได้หมายความว่าคุณควรคัดลอกรหัสเดียวกันลงในแต่ละpublicวิธี


3
  1. พวกเขาไม่ควรทำลายโดยการเปลี่ยนแปลงรหัสที่ไม่เกี่ยวข้องใดๆ ใน codebase

ฉันไม่แน่ใจจริงๆว่ากฎนี้มีประโยชน์อย่างไร หากการเปลี่ยนแปลงในคลาส / วิธี / สิ่งใดก็ตามที่สามารถทำลายพฤติกรรมของผู้อื่นในรหัสการผลิตแล้วสิ่งต่าง ๆ ก็คือในความเป็นจริงผู้ทำงานร่วมกันและไม่เกี่ยวข้องกัน หากการทดสอบของคุณหยุดและรหัสการผลิตของคุณไม่แสดงว่าการทดสอบของคุณเป็นที่น่าสงสัย

  1. การทดสอบหน่วยเดียวควรทำลายข้อบกพร่องในหน่วยการทดสอบซึ่งตรงข้ามกับการทดสอบการรวม (ซึ่งอาจแบ่งเป็นฮีป)

ฉันถือว่ากฎนี้ด้วยความสงสัยเช่นกัน หากคุณดีพอที่จะจัดโครงสร้างโค้ดของคุณและเขียนการทดสอบของคุณเพื่อที่ว่าบั๊กหนึ่งตัวจะทำให้การทดสอบหน่วยเดียวล้มเหลวคุณก็จะบอกว่าคุณได้ระบุบั๊กที่อาจเกิดขึ้นทั้งหมดแล้วแม้ในขณะที่ codebase วิวัฒนาการ ยังไม่ได้คาดหวัง

เส้นแบ่งระหว่างพวกเขากับการทดสอบการรวมอยู่ตรงไหน?

ฉันไม่คิดว่ามันเป็นความแตกต่างที่สำคัญ โค้ด 'หน่วย' คืออะไร?

พยายามหาจุดเข้าใช้งานที่คุณสามารถเขียนการทดสอบที่ 'สมเหตุสมผล' ในแง่ของปัญหาโดเมน / กฎธุรกิจที่ระดับของรหัสนั้นเกี่ยวข้อง บ่อยครั้งที่การทดสอบเหล่านี้ค่อนข้าง 'ใช้งานได้' ในธรรมชาติ - ใส่ในอินพุตและทดสอบว่าเอาต์พุตเป็นไปตามที่คาดไว้ หากการทดสอบแสดงพฤติกรรมที่ต้องการของระบบพวกเขามักจะยังคงมีเสถียรภาพแม้ในขณะที่รหัสการผลิตวิวัฒนาการและ refactored

วิธีการทดสอบหน่วยควรจะเขียนโดยไม่เยาะเย้ยอย่างกว้างขวาง?

อย่าอ่านมากเกินไปในคำว่า 'หน่วย' และเรียนรู้ที่จะใช้คลาสการผลิตจริงของคุณในการทดสอบโดยไม่ต้องกังวลมากเกินไปถ้าคุณเกี่ยวข้องมากกว่าหนึ่งในการทดสอบ หากหนึ่งในนั้นใช้งานยาก (เพราะใช้เวลาในการเริ่มต้นจำนวนมากหรือต้องใช้ฐานข้อมูลจริง / เซิร์ฟเวอร์อีเมล ฯลฯ ) จากนั้นให้ความคิดของคุณเปลี่ยนเป็นการเยาะเย้ย / แกล้งทำ


"รหัส" หน่วยคืออะไร "คำถามที่ดีมากที่สามารถมีคำตอบที่ไม่คาดคิดซึ่งอาจขึ้นอยู่กับว่าใครตอบ โดยทั่วไปแล้วคำจำกัดความของการทดสอบหน่วยส่วนใหญ่อธิบายให้พวกเขาเกี่ยวกับวิธีการหรือชั้นเรียน แต่นั่นไม่ใช่การวัดที่มีประโยชน์จริง ๆ ของ "หน่วย" ในทุกกรณี ถ้าฉันมีPerson:tellStory()วิธีการรวมรายละเอียดของบุคคลเป็นสตริงแล้วส่งคืนนั้น "เรื่องราว" อาจเป็นหนึ่งหน่วย หากฉันสร้างวิธีการช่วยเหลือแบบส่วนตัวที่กำจัดโค้ดบางส่วนออกไปฉันไม่เชื่อว่าฉันได้แนะนำหน่วยใหม่ - ฉันไม่จำเป็นต้องทดสอบแยกต่างหาก
VLAZ

1

ก่อนคำจำกัดความบางอย่าง:

การทดสอบหน่วยทดสอบหน่วยที่แยกออกจากหน่วยอื่น ๆแต่สิ่งที่หมายถึงไม่ได้กำหนดไว้อย่างชัดเจนโดยแหล่งที่มีสิทธิ์ใด ๆ ดังนั้นขอให้กำหนดมันได้ดีกว่า: ถ้าขอบเขต I / O ถูกข้าม (ไม่ว่า I / O เป็นเครือข่ายดิสก์หรือไม่ หน้าจอหรืออินพุต UI) มีจุดกึ่งกลางที่เราสามารถวาดเส้นได้ หากรหัสขึ้นอยู่กับ I / O รหัสจะข้ามเขตแดนของหน่วยและดังนั้นจะต้องจำลองหน่วยที่รับผิดชอบสำหรับ I / O นั้น

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

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

รหัสกลิ่น

สำหรับสิ่งนี้เราจะเปลี่ยนเป็น Wikipedia:

ในการเขียนโปรแกรมคอมพิวเตอร์กลิ่นรหัสเป็นลักษณะใด ๆ ในซอร์สโค้ดของโปรแกรมที่อาจบ่งบอกถึงปัญหาที่ลึกกว่า

มันจะดำเนินต่อไปในภายหลัง ...

"กลิ่นเป็นโครงสร้างบางอย่างในรหัสที่บ่งบอกถึงการละเมิดหลักการออกแบบพื้นฐานและส่งผลเสียต่อคุณภาพการออกแบบ" Suryanarayana, Girish (พฤศจิกายน 2014) การปรับโครงสร้างใหม่สำหรับกลิ่นการออกแบบซอฟต์แวร์ มอร์แกน Kaufmann พี 258

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

กล่าวอีกนัยหนึ่งไม่ได้รหัสกลิ่นทั้งหมดจะไม่ดี แต่เป็นสิ่งบ่งชี้ทั่วไปว่ามีบางสิ่งที่ไม่สามารถแสดงออกมาในรูปแบบที่เหมาะสมและกลิ่นอาจบ่งบอกถึงโอกาสในการปรับปรุงรหัสที่เป็นปัญหา

ในกรณีของการเยาะเย้ยกลิ่นบ่งบอกว่าหน่วยที่ดูเหมือนจะเรียก mocks ขึ้นอยู่กับหน่วยที่จะเยาะเย้ย อาจเป็นข้อบ่งชี้ว่าเราไม่ได้จำแนกปัญหาออกเป็นชิ้นส่วนที่สามารถแก้ไขได้ด้วยอะตอมและนั่นอาจบ่งบอกถึงข้อบกพร่องในการออกแบบในซอฟต์แวร์

สาระสำคัญของการพัฒนาซอฟต์แวร์ทั้งหมดคือกระบวนการแยกปัญหาใหญ่ออกเป็นชิ้นเล็กชิ้นเล็กชิ้นน้อย (การสลายตัว) และรวมวิธีการแก้ปัญหาเข้าด้วยกันเพื่อสร้างแอปพลิเคชั่นที่แก้ปัญหาใหญ่ (องค์ประกอบ)

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

สิ่งที่ทำให้การเยาะเย้ยรหัสกลิ่นไม่ใช่ว่ามีอะไรผิดปกติกับการเยาะเย้ย - บางครั้งมันมีประโยชน์มาก สิ่งที่ทำให้มีกลิ่นรหัสคือมันสามารถระบุแหล่งที่มาของการมีเพศสัมพันธ์ที่มีปัญหาในใบสมัครของคุณ บางครั้งการลบแหล่งที่มาของการแต่งงานกันนั้นมีประสิทธิผลมากกว่าการเขียนล้อเลียน

การมีเพศสัมพันธ์มีหลายประเภทและบางอย่างดีกว่าอย่างอื่น การเข้าใจว่า mocks เป็นกลิ่นรหัสสามารถสอนให้คุณระบุและหลีกเลี่ยงชนิดที่เลวร้ายที่สุดในช่วงต้นของวงจรการออกแบบแอปพลิเคชันก่อนที่กลิ่นจะพัฒนาเป็นสิ่งที่แย่กว่านั้น


0

ควรใช้การเยาะเย้ยเป็นทางเลือกสุดท้ายแม้ในการทดสอบหน่วย

วิธีการไม่ได้เป็นหน่วยและแม้กระทั่งชั้นไม่ได้เป็นหน่วย หน่วยคือการแยกลอจิกของโค้ดที่สมเหตุสมผลไม่ว่าคุณจะเรียกมันว่าอะไร องค์ประกอบที่สำคัญของการมีโค้ดที่ผ่านการทดสอบอย่างดีคือการสามารถรีแฟคเตอร์ได้อย่างอิสระและส่วนหนึ่งของการรีแฟคเตอร์ได้อย่างอิสระหมายความว่าคุณไม่จำเป็นต้องเปลี่ยนการทดสอบเพื่อที่จะทำเช่นนั้น ยิ่งคุณล้อเลียนยิ่งคุณต้องเปลี่ยนการทดสอบของคุณเมื่อคุณ refactor หากคุณพิจารณาวิธีการเรียนรู้ของหน่วยคุณต้องเปลี่ยนการทดสอบทุกครั้งที่คุณปรับโครงสร้าง และถ้าคุณพิจารณาหน่วยการเรียนนั้นคุณจะต้องเปลี่ยนการทดสอบทุกครั้งที่คุณต้องการแบ่งชั้นเรียนเป็นหลายชั้น เมื่อคุณต้อง refactor การทดสอบของคุณเพื่อ refactor รหัสของคุณมันทำให้คนเลือกที่จะไม่ refactor รหัสของพวกเขาซึ่งเป็นเพียงเกี่ยวกับสิ่งที่เลวร้ายที่สุดที่สามารถเกิดขึ้นกับโครงการ จำเป็นอย่างยิ่งที่คุณจะต้องแบ่งชั้นเรียนออกเป็นหลาย ๆ คลาสโดยไม่ต้องปรับโครงสร้างการทดสอบของคุณอีกครั้งหรือคุณจะจบลงด้วยคลาสปาเก็ตตี้ขนาดใหญ่ 500 บรรทัด หากคุณกำลังใช้วิธีการหรือคลาสเป็นหน่วยของคุณด้วยการทดสอบหน่วยคุณอาจไม่ได้ทำการเขียนโปรแกรมเชิงวัตถุ แต่เป็นการเขียนโปรแกรมที่ใช้งานได้บางอย่างกับวัตถุ

การแยกรหัสของคุณสำหรับการทดสอบหน่วยไม่ได้หมายความว่าคุณเยาะเย้ยทุกอย่างภายนอก ถ้าเป็นเช่นนั้นคุณต้องจำลองวิชาคณิตศาสตร์ในภาษาของคุณและไม่มีใครคิดว่าเป็นความคิดที่ดี การพึ่งพาภายในไม่ควรได้รับการปฏิบัติแตกต่างจากการพึ่งพาภายนอก คุณเชื่อมั่นว่าพวกเขาได้รับการทดสอบอย่างดีและทำงานได้อย่างที่ควรจะเป็น ข้อแตกต่างที่แท้จริงคือถ้าการพึ่งพาภายในของคุณทำลายโมดูลของคุณคุณสามารถหยุดสิ่งที่คุณกำลังทำเพื่อแก้ไขแทนที่จะต้องไปโพสต์ปัญหาบน GitHub และขุดลงใน codebase ที่คุณไม่เข้าใจเพื่อแก้ไข หรือหวังว่าจะดีที่สุด

การแยกรหัสของคุณหมายความว่าคุณปฏิบัติต่อการพึ่งพาภายในของคุณเช่นกล่องดำและไม่ทดสอบสิ่งที่เกิดขึ้นภายในพวกเขา หากคุณมีโมดูล B ซึ่งรับอินพุต 1, 2 หรือ 3 และคุณมีโมดูล A ซึ่งเรียกว่าคุณไม่ต้องทดสอบโมดูล A ทำแต่ละตัวเลือกเหล่านี้คุณเพียงแค่เลือกและใช้สิ่งนั้น หมายความว่าการทดสอบโมดูล A ของคุณควรทดสอบวิธีการต่าง ๆ ที่คุณปฏิบัติต่อการตอบสนองจากโมดูล B ไม่ใช่สิ่งที่คุณส่งผ่าน

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

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