การทดสอบหน่วยเปราะเนื่องจากต้องการการเยาะเย้ยมากเกินไป


21

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

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

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

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

นี่คือตัวอย่างการทดสอบหน่วยที่จับปัญหา

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

[TestMethod]
public void VerifyMergeStopsSpinner()
{
    var mockViewModel = new Mock<MergeTests> { CallBase = true };
    var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());

    mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
    mockViewModel.Setup(
        m =>
        m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
                         It.IsAny<bool>()));
    mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
    mockViewModel.Setup(m => m.SwitchToOverviewTab());
    mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
    mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
    mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));

    mockViewModel.Object.OnMerge(It.IsAny<MergeState>());    

    mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}

ส่วนที่เหลือของคุณจัดการกับเรื่องนี้อย่างไรหรือไม่มีวิธีการ 'จัดการ' ที่ยอดเยี่ยมอย่างนี้?

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


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

มันยากที่จะเห็น แต่ Mock <MergeTests> คือ SUT เราตั้งค่าสถานะ CallBase เพื่อให้แน่ใจว่าวิธีการ 'OnMerge' ดำเนินการกับวัตถุจริง แต่จำลองวิธีการที่เรียกว่า 'OnMerge' ที่อาจทำให้การทดสอบล้มเหลวเนื่องจากปัญหาการพึ่งพาเป็นต้นเป้าหมายของการทดสอบคือบรรทัดสุดท้าย - เพื่อยืนยันว่าเราหยุดเครื่องปั่นด้ายในกรณีนี้
PremiumTier

MergeTests ดูเหมือนจะเป็นเครื่องดนตรีประเภทอื่นไม่ใช่สิ่งที่มีชีวิตอยู่ในการผลิตดังนั้นความสับสน
Joppe


1
นอกเหนือจากปัญหาอื่น ๆ ของคุณดูเหมือนว่าฉันผิดที่ SUT ของคุณคือการเยาะเย้ย <MergeTests> ทำไมคุณต้องทดสอบการเยาะเย้ย? ทำไมคุณไม่ทดสอบคลาส MergeTests?
Eric King

คำตอบ:


18
  1. แก้ไขรหัสเพื่อให้ออกแบบได้ดีขึ้น หากการทดสอบของคุณมีปัญหาเหล่านี้รหัสของคุณจะมีปัญหาแย่ลงเมื่อคุณพยายามเปลี่ยนสิ่งต่าง ๆ

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

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


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

หากการออกแบบที่ดีกว่าไม่เป็นจริงฉันเห็นด้วยกับ 'ใครสนใจถ้าคุณใช้วิธีอื่น 5 วิธี' ตรวจสอบว่าวิธีการทำหน้าที่ที่จำเป็นไม่ใช่วิธีการทำ
Kwebble

@Kwebble - เข้าใจ แต่เป้าหมายของคำถามคือการตรวจสอบว่ามีวิธีง่ายๆในการตรวจสอบพฤติกรรมสำหรับวิธีการหรือไม่เมื่อคุณต้องจำลองพฤติกรรมอื่น ๆ ที่เรียกว่าภายในวิธีการเพื่อให้สามารถทำการทดสอบได้เลย ฉันต้องการลบ 'อย่างไร' แต่ฉันไม่ทราบวิธี :)
PremiumTier

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

8

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

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

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


3

ฉันจะพิจารณาทำให้สบายใน mocks และเพียงแค่กำหนดแบบทดสอบที่อาจรวมถึงวิธีการที่เรียกร้องให้

อย่าทดสอบวิธีการทดสอบสิ่งที่ เป็นผลลัพธ์ที่มีความสำคัญรวมถึงวิธีการย่อยหากจำเป็น

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

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


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

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

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