หลักการออกแบบที่ส่งเสริมโค้ดที่ทดสอบได้คืออะไร (การออกแบบรหัสที่ทดสอบได้และการออกแบบการขับขี่ผ่านการทดสอบ)


54

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

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

ฉันได้อ่านแล้วว่ามีบางสิ่งที่เรียกว่าโซลิด ฉันต้องการที่จะเข้าใจว่าหากทำตามหลักการของ SOLID จะส่งผลทางอ้อมในรหัสที่สามารถทดสอบได้ง่ายหรือไม่? ถ้าไม่มีมีหลักการออกแบบที่ชัดเจนที่ส่งเสริมรหัสที่สามารถทดสอบได้หรือไม่

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

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


3
ดูที่นี่: googletesting.blogspot.in/2008/08/…
VS1

ขอขอบคุณ. ฉันเพิ่งเริ่มอ่านบทความและมันก็สมเหตุสมผลแล้ว

1
นี่เป็นหนึ่งในคำถามสัมภาษณ์ของฉัน ("คุณออกแบบรหัสเพื่อทดสอบหน่วยได้อย่างง่ายดายได้อย่างไร") มันแสดงให้ฉันเห็นอย่างถี่ถ้วนว่าพวกเขาเข้าใจการทดสอบหน่วยการเยาะเย้ย / การขัดถู OOD และ TDD ที่อาจเกิดขึ้น น่าเศร้าที่คำตอบมักจะเป็น "สร้างฐานข้อมูลทดสอบ"
Chris Pitman

คำตอบ:


56

ใช่ SOLID เป็นวิธีที่ดีมากในการออกแบบรหัสที่สามารถทดสอบได้ง่าย ในฐานะที่เป็นสีรองพื้นสั้น:

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

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

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

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

หลักการทดแทน L - Liskov: คลาส A ขึ้นอยู่กับคลาส B ควรใช้ X: B ใด ๆ โดยไม่ทราบความแตกต่าง นี่หมายความว่าโดยทั่วไปแล้วสิ่งใดก็ตามที่คุณใช้เป็นผู้อ้างอิงควรมีพฤติกรรมที่คล้ายกันตามที่เห็นโดยคลาสที่พึ่งพา เป็นตัวอย่างสั้น ๆ สมมติว่าคุณมีอินเทอร์เฟซ IWriter ที่ exposes เขียน (สตริง) ซึ่งถูกนำมาใช้โดย ConsoleWriter ตอนนี้คุณต้องเขียนไปยังไฟล์แทนดังนั้นคุณจึงสร้าง FileWriter ในการทำเช่นนั้นคุณต้องตรวจสอบให้แน่ใจว่าสามารถใช้ FileWriter ได้เช่นเดียวกับที่ ConsoleWriter ทำ (หมายถึงวิธีเดียวที่ผู้พึ่งพาสามารถโต้ตอบกับมันได้โดยการเรียกการเขียน (สตริง)) และข้อมูลเพิ่มเติมที่ FileWriter อาจต้องทำ job (เช่นพา ธ และไฟล์ที่จะเขียน) ต้องจัดเตรียมจากที่อื่นนอกเหนือจากที่ขึ้นอยู่กับ

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

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

การยึดมั่นใน ISP ช่วยปรับปรุงความสามารถในการทดสอบได้โดยลดความซับซ้อนของระบบภายใต้การทดสอบและการพึ่งพาของ SUT เหล่านั้น หากวัตถุที่คุณกำลังทดสอบขึ้นอยู่กับอินเทอร์เฟซ IDoThreeThings ซึ่งแสดง DoOne (), DoTwo () และ DoThree () คุณต้องจำลองวัตถุที่ใช้วิธีการทั้งสามแม้ว่าวัตถุนั้นจะใช้วิธีการ DoTwo เท่านั้น แต่ถ้าวัตถุนั้นขึ้นอยู่กับ IDoTwo เท่านั้น (ซึ่งจะเปิดเผยเฉพาะ DoTwo) คุณสามารถจำลองวัตถุที่มีวิธีการนั้นได้ง่ายขึ้น

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

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


16

ฉันได้อ่านแล้วว่ามีบางสิ่งที่เรียกว่าโซลิด ฉันต้องการที่จะเข้าใจว่าหากทำตามหลักการของ SOLID จะส่งผลทางอ้อมในรหัสที่สามารถทดสอบได้ง่ายหรือไม่?

หากใช้อย่างถูกต้องใช่ มีบล็อกโพสต์โดย Jeffอธิบายหลักการ SOLID ในทางสั้น ๆจริงๆ (พอดคาสต์ที่กล่าวถึงนั้นควรฟังด้วย) ฉันขอแนะนำให้ดูที่นั่นหากคำอธิบายอีกต่อไปกำลังสลัดคุณอยู่

จากประสบการณ์ของฉันหลักการ 2 ข้อจาก SOLID มีบทบาทสำคัญในการออกแบบรหัสที่ทดสอบได้:

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

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

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

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

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

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


iaw การเชื่อมโยงสูงและการมีเพศสัมพันธ์ต่ำ
jk

8

คำถามแรกของคุณ:

โซลิดเป็นวิธีที่จะไป ฉันพบว่าตัวย่อที่สำคัญที่สุดสองประการของ SOLID เมื่อพูดถึงการทดสอบคือ S (ความรับผิดชอบเดี่ยว) และ D (การพึ่งพาการฉีด)

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

Dependency Injection (DI):สิ่งนี้ให้คุณควบคุมสภาพแวดล้อมการทดสอบ แทนที่จะสร้างการลงทะเบียนวัตถุภายในรหัสของคุณคุณสามารถแทรกมันผ่านตัวสร้างคลาสหรือการเรียกเมธอด เมื่อไม่ตั้งค่าคุณเพียงแทนที่คลาสจริงด้วยสตับหรือ mocks ซึ่งคุณสามารถควบคุมได้ทั้งหมด

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


4

นอกเหนือจากคำตอบอื่น ๆ ซึ่งมุ่งเน้นไปที่การมีเพศสัมพันธ์แบบหลวม ๆ ฉันอยากจะพูดเกี่ยวกับการทดสอบตรรกะที่ซับซ้อน

ฉันเคยต้องทดสอบหน่วยที่มีตรรกะที่ซับซ้อนมีเงื่อนไขมากมายและยากที่จะเข้าใจบทบาทของทุ่ง

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

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

แน่นอนว่าไม่ใช่ทุก ๆ ตรรกะที่ซับซ้อนที่สามารถสร้างแบบจำลองเป็นเครื่องรัฐ


3

SOLID เป็นการเริ่มต้นที่ดีเยี่ยมจากประสบการณ์ของฉันสี่ด้านของ SOLID นั้นใช้ได้ดีกับการทดสอบหน่วย

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

ฉันจะดูเป็นลวดลายต่าง ๆ โดยเฉพาะลวดลายโรงงาน สมมติว่าคุณมีคลาสที่เป็นรูปธรรมที่ใช้อินเทอร์เฟซ คุณจะสร้างโรงงานเพื่อยกตัวอย่างคลาสคอนกรีต แต่กลับอินเตอร์เฟซแทน

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

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

หากคุณต้องการที่จะขยายตัวนี้บิตผมขอแนะนำให้อ่านศิลปะของการทดสอบหน่วย มันเป็นตัวอย่างที่ดีเกี่ยวกับวิธีใช้หลักการนี้และเป็นการอ่านที่รวดเร็ว


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