แนวทางปฏิบัติที่ดีที่สุดของการพัฒนาแบบทดสอบโดยใช้ C # และ RhinoMocks [ปิด]


87

เพื่อช่วยทีมของฉันเขียนโค้ดที่สามารถทดสอบได้ฉันจึงได้จัดทำรายการแนวทางปฏิบัติที่ดีที่สุดง่ายๆนี้เพื่อทำให้ฐานรหัส C # ของเราสามารถทดสอบได้มากขึ้น (บางประเด็นอ้างถึงข้อ จำกัด ของ Rhino Mocks ซึ่งเป็นกรอบการเยาะเย้ยสำหรับ C # แต่กฎอาจมีผลบังคับใช้มากขึ้นเช่นกัน) ใครมีแนวทางปฏิบัติที่ดีที่สุดที่พวกเขาปฏิบัติตามหรือไม่?

ในการเพิ่มความสามารถในการทดสอบโค้ดให้ปฏิบัติตามกฎเหล่านี้:

  1. เขียนแบบทดสอบก่อนแล้วจึงเขียนโค้ด เหตุผล: เพื่อให้แน่ใจว่าคุณเขียนโค้ดที่ทดสอบได้และโค้ดทุกบรรทัดจะได้รับการทดสอบที่เขียนขึ้น

  2. ออกแบบคลาสโดยใช้การฉีดแบบพึ่งพา เหตุผล: คุณไม่สามารถล้อเลียนหรือทดสอบสิ่งที่มองไม่เห็นได้

  3. แยกโค้ด UI ออกจากพฤติกรรมโดยใช้ Model-View-Controller หรือ Model-View-Presenter เหตุผล: อนุญาตให้ทดสอบตรรกะทางธุรกิจในขณะที่ย่อส่วนที่ไม่สามารถทดสอบได้ (UI)

  4. อย่าเขียนวิธีการหรือคลาสแบบคงที่ เหตุผล: วิธีการคงที่เป็นเรื่องยากหรือเป็นไปไม่ได้ที่จะแยกออกและ Rhino Mocks ไม่สามารถล้อเลียนได้

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

  6. แยกการอ้างอิงภายนอก เหตุผล: ไม่สามารถทดสอบการอ้างอิงภายนอกที่ไม่ได้รับการแก้ไข

  7. ทำเครื่องหมายเป็นวิธีเสมือนจริงที่คุณตั้งใจจะล้อเลียน เหตุผล: Rhino Mocks ไม่สามารถล้อเลียนวิธีการที่ไม่ใช่เสมือนจริงได้


นี่คือรายการที่มีประโยชน์ ขณะนี้เราใช้ NUnit และ Rhino.Mocks และเป็นการดีที่จะระบุเกณฑ์เหล่านี้สำหรับสมาชิกในทีมที่ไม่ค่อยคุ้นเคยกับการทดสอบหน่วยด้านนี้
Chris Ballard

คำตอบ:


58

รายการที่ดีแน่นอน นี่คือความคิดบางประการเกี่ยวกับเรื่องนี้:

เขียนแบบทดสอบก่อนแล้วจึงเขียนโค้ด

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

ออกแบบคลาสโดยใช้การฉีดแบบพึ่งพา

เห็นด้วย เมื่อออบเจ็กต์สร้างการอ้างอิงของตัวเองคุณจะไม่สามารถควบคุมสิ่งเหล่านั้นได้ Inversion of Control / Dependency Injection ช่วยให้คุณสามารถควบคุมได้ช่วยให้คุณสามารถแยกวัตถุที่อยู่ระหว่างการทดสอบด้วย mocks / stubs / etc นี่คือวิธีทดสอบออบเจ็กต์แยกกัน

แยกรหัส UI ออกจากลักษณะการทำงานโดยใช้ Model-View-Controller หรือ Model-View-Presenter

ตกลง โปรดทราบว่าแม้แต่ผู้นำเสนอ / ตัวควบคุมก็สามารถทดสอบโดยใช้ DI / IoC ได้โดยมอบมุมมองและโมเดลที่ถูกตัดต่อ / เยาะเย้ย ลองดูPresenter First TDD สำหรับข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนั้น

อย่าเขียนวิธีการหรือคลาสแบบคงที่

ไม่แน่ใจว่าฉันเห็นด้วยกับข้อนี้ เป็นไปได้ที่จะทดสอบหน่วยวิธี / คลาสแบบคงที่โดยไม่ต้องใช้ล้อเลียน ดังนั้นนี่อาจเป็นหนึ่งในกฎเฉพาะของ Rhino Mock ที่คุณกล่าวถึง

โปรแกรมปิดอินเทอร์เฟซไม่ใช่คลาส

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

แยกการอ้างอิงภายนอก

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

ทำเครื่องหมายเป็นวิธีเสมือนจริงที่คุณตั้งใจจะล้อเลียน

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

และมีสองประเด็นใหม่ที่ต้องพิจารณา:

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

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

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

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

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


3
ฉันมักจะพบว่าถ้าการทดสอบนั้นอ่านยากไม่ใช่ความผิดของเฟรมเวิร์ก แต่เป็นการทดสอบโค้ด ถ้ามทส. มีความซับซ้อนในการจัดตั้งอาจจะต้องแยกเป็นแนวคิดมากกว่านี้
Steve Freeman

10

หากคุณกำลังทำงานกับ. Net 3.5 คุณอาจต้องการดูในไลบรารีการเยาะเย้ยMoqซึ่งใช้แผนภูมินิพจน์และ lambdas เพื่อลบสำนวนการตอบกลับบันทึกที่ไม่ใช้งานง่ายของไลบรารีจำลองอื่น ๆ ส่วนใหญ่

ลองดูการเริ่มต้นอย่างรวดเร็วนี้เพื่อดูว่ากรณีทดสอบของคุณใช้งานง่ายมากขึ้นเพียงใดต่อไปนี้เป็นตัวอย่างง่ายๆ:

// ShouldExpectMethodCallWithVariable
int value = 5;
var mock = new Mock<IFoo>();

mock.Expect(x => x.Duplicate(value)).Returns(() => value * 2);

Assert.AreEqual(value * 2, mock.Object.Duplicate(value));

5
ฉันคิดว่า Rhino Mocks เวอร์ชันใหม่ก็ใช้ได้เช่นกัน
George Mauer


3

นี่เป็นโพสต์ที่มีประโยชน์มาก!

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

สำหรับบางท่านที่อาศัยอยู่ในโลกแห่งการศึกษาระยะเวลาและการส่งมอบอาจไม่สำคัญ แต่ในสภาพแวดล้อมที่ซอฟต์แวร์เป็นเงินเป็นทองการใช้ความพยายาม TDD อย่างมีประสิทธิภาพเป็นสิ่งสำคัญ

TDD อยู่ภายใต้กฎหมายว่าด้วยการลดผลตอบแทนเล็กน้อยกลับ ในระยะสั้นความพยายามของคุณที่มีต่อ TDD จะมีค่ามากขึ้นเรื่อย ๆ จนกว่าคุณจะได้รับผลตอบแทนสูงสุดหลังจากนั้นการลงทุนใน TDD ในครั้งต่อ ๆ ไปมีมูลค่าน้อยลงเรื่อย ๆ

ฉันมักจะเชื่อว่าค่าหลักของ TDD อยู่ในขอบเขต (blackbox) เช่นเดียวกับในการทดสอบ whitebox ของพื้นที่สำคัญของระบบเป็นครั้งคราว


2

เหตุผลที่แท้จริงในการเขียนโปรแกรมเทียบกับอินเทอร์เฟซไม่ใช่เพื่อให้ชีวิต Rhino ง่ายขึ้น แต่เพื่อชี้แจงความสัมพันธ์ระหว่างวัตถุในโค้ด อินเทอร์เฟซควรกำหนดเซอร์วิสที่อ็อบเจ็กต์ต้องการจากสภาพแวดล้อม คลาสจัดเตรียมการนำบริการนั้นไปใช้งานโดยเฉพาะ อ่านหนังสือ "Object Design" ของ Rebecca Wirfs-Brock เรื่องบทบาทความรับผิดชอบและผู้ทำงานร่วมกัน


ตกลง ... ฉันจะอัปเดตคำถามเพื่อสะท้อนให้เห็นว่า
Kevin Albrecht

1

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

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


ฉันยังทำส่วนการทดสอบสไตล์ BDD ที่คล้ายกัน (นอกเหนือจากรหัสการทดสอบหน่วย) ในโครงการส่วนตัว
Kevin Albrecht

0

นี่เป็นอีกหนึ่งสิ่งที่ฉันคิดว่าฉันชอบทำ

หากคุณวางแผนที่จะเรียกใช้การทดสอบจากหน่วยทดสอบ Gui ซึ่งตรงข้ามกับ TestDriven.Net หรือ NAnt ฉันพบว่าการตั้งค่าประเภทโครงการทดสอบหน่วยเป็นแอปพลิเคชันคอนโซลมากกว่าไลบรารีนั้นง่ายกว่า สิ่งนี้ช่วยให้คุณเรียกใช้การทดสอบด้วยตนเองและทำตามขั้นตอนเหล่านี้ในโหมดดีบัก (ซึ่ง TestDriven.Net ดังกล่าวข้างต้นสามารถทำเพื่อคุณได้)

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

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