จะหลีกเลี่ยงการทดสอบหน่วยเปราะบางได้อย่างไร


24

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

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

กรอบ


นี่เหมาะกว่า Programmers.StackExchange, IMO ...
IAbstract

คำตอบ:


21

อย่าคิดว่าพวกเขาเป็น "การทดสอบหน่วยที่ขาด" เพราะไม่ใช่

นี่เป็นข้อกำหนดซึ่งโปรแกรมของคุณไม่สนับสนุนอีกต่อไป

อย่าคิดว่าเป็น "การแก้ไขการทดสอบ" แต่เป็น "การกำหนดข้อกำหนดใหม่"

การทดสอบควรระบุใบสมัครของคุณก่อนไม่ใช่วิธีอื่น

คุณไม่สามารถพูดได้ว่าคุณมีการนำไปใช้งานจนกว่าคุณจะรู้ว่ามันใช้งานได้ คุณไม่สามารถพูดได้ว่ามันใช้งานได้จนกว่าคุณจะทดสอบ

หมายเหตุอื่น ๆ ที่อาจแนะนำคุณ:

  1. การทดสอบและการเรียนภายใต้การทดสอบควรจะสั้นและง่าย การทดสอบแต่ละครั้งควรตรวจสอบฟังก์ชั่นที่เหนียวแน่นเท่านั้น นั่นคือมันไม่สนใจเกี่ยวกับสิ่งที่การทดสอบอื่น ๆ ตรวจสอบแล้ว
  2. การทดสอบและวัตถุของคุณควรเชื่อมโยงกันอย่างหลวม ๆ ในลักษณะที่ถ้าคุณเปลี่ยนวัตถุคุณเพียง แต่เปลี่ยนกราฟการพึ่งพาของมันลงและวัตถุอื่น ๆ ที่ใช้วัตถุนั้นจะไม่ได้รับผลกระทบ
  3. คุณอาจจะสร้างและทดสอบสิ่งที่ไม่ถูกต้อง วัตถุของคุณถูกสร้างขึ้นเพื่อการเชื่อมต่อที่ง่ายหรือง่ายในการติดตั้งหรือไม่? หากเป็นกรณีหลังคุณจะพบว่าตัวเองกำลังเปลี่ยนรหัสจำนวนมากที่ใช้อินเทอร์เฟซของการใช้งานแบบเก่า
  4. ในกรณีที่ดีที่สุดให้ยึดมั่นในหลักการความรับผิดชอบเดี่ยวอย่างเคร่งครัด ในกรณีที่แย่กว่านั้นให้ปฏิบัติตามหลักการแยกส่วนต่อประสาน ดูหลักการ SOLID

5
+1 สำหรับDon't think of it as "fixing the tests", but as "defining new requirements".
StuperUser

2
+1 การทดสอบควรระบุแอปพลิเคชันของคุณก่อนไม่ใช่แบบอื่น
treecoder

11

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

เมื่อระบบมีการเปลี่ยนแปลงเราพบว่าเราใช้เวลามากขึ้นในการแก้ไขข้อผิดพลาด เรามีการทดสอบหน่วยการรวมและการทำงาน

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

ข้อมูลถูกเข้ารหัสอย่างหนัก

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

assert sum([1,2,3]) == 6
assert sum([1,2,3]) == 1 + 2 + 3
assert sum([1,2,3]) == reduce(operator.add, [1,2,3])

ยิ่งคุณใช้ Abstraction มากเท่าไหร่คุณยิ่งเข้าใกล้อัลกอริธึมและยิ่งใกล้เคียงกับการเปรียบเทียบการใช้งานแบบเฉียบพลันกับตัวมันเอง

การใช้รหัสซ้ำเพียงเล็กน้อย

การนำโค้ดกลับมาใช้ใหม่ได้ดีที่สุดในการทดสอบคือ imho 'Checks' เช่นเดียวกับใน jUnits assertThatเพราะมันทำให้การทดสอบง่ายขึ้น นอกจากนั้นหากการทดสอบสามารถ refactored รหัสหุ้นรหัสจริงการทดสอบมีแนวโน้มที่สามารถเกินไปซึ่งช่วยลดการทดสอบกับคนที่ทดสอบฐาน refactored


ฉันต้องการทราบว่าผู้ลงคะแนนเสียงไม่เห็นด้วยหรือไม่
keppla

keppla - ฉันไม่ใช่ downvoter แต่โดยทั่วไปขึ้นอยู่กับว่าฉันอยู่ที่ไหนในแบบจำลองฉันชอบทดสอบการโต้ตอบกับออบเจ็กต์มากกว่าข้อมูลการทดสอบในระดับหน่วย ข้อมูลการทดสอบทำงานได้ดีขึ้นในระดับการรวม
Ritch Melton

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

6

ฉันมีปัญหานี้เช่นกัน วิธีการปรับปรุงของฉันมีดังนี้:

  1. อย่าเขียนการทดสอบหน่วยเว้นแต่จะเป็นวิธีการทดสอบที่ดีเท่านั้น

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

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

    ก่อนที่จะมีคนฆ่าฉัน: งานสร้างงานสร้างไม่ควรผิดพลาดในการยืนยัน พวกเขาควรเข้าสู่ระบบในระดับ "ข้อผิดพลาด" แทน

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

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

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

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

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

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


ในเรื่องของ "อย่าเขียนการทดสอบหน่วย" ฉันจะนำเสนอตัวอย่าง:

TEST(exception_thrown_on_null)
{
    InternalDataStructureType sink;
    ASSERT_THROWS(sink.consumeFrom(NULL), std::logic_error);
    try {
        sink.consumeFrom(NULL);
    } catch (const std::logic_error& e) {
        ASSERT(e.what() == "You must not pass NULL as a parameter!");
    }
}

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

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

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

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


3
ชอบการทดสอบระบบ / การรวมเข้าด้วยกัน - สิ่งนี้ไม่ดีต่อจิตใจ ระบบของคุณไปถึงจุดที่ใช้การทดสอบ (ช้า!) เหล่านี้เพื่อทดสอบสิ่งที่สามารถจับได้อย่างรวดเร็วในระดับหน่วยและใช้เวลาหลายชั่วโมงกว่าจะทำงานได้เพราะคุณมีการทดสอบที่คล้ายกันและช้ามาก
Ritch Melton

1
@RitchMelton แยกจากการสนทนาโดยสิ้นเชิงดูเหมือนว่าคุณต้องการเซิร์ฟเวอร์ CI ใหม่ CI ไม่ควรทำตัวแบบนั้น
Andres Jaan Tack

1
โปรแกรมหยุดทำงาน (ซึ่งเป็นสิ่งที่ยืนยันทำ) ไม่ควรฆ่านักทดสอบของคุณ (CI) นั่นเป็นเหตุผลที่คุณมีนักวิ่งทดสอบ ดังนั้นสิ่งที่สามารถตรวจจับและรายงานความล้มเหลวดังกล่าว
Andres Jaan Tack

1
การยืนยันแบบ 'ยืนยันเท่านั้น' สไตล์ที่ฉันคุ้นเคยกับ (ไม่ใช่การทดสอบการยืนยัน) ปรากฏขึ้นกล่องโต้ตอบที่แฮง CI เพราะกำลังรอการโต้ตอบของนักพัฒนา
Ritch Melton

1
อ่านั่นจะอธิบายอะไรมากมายเกี่ยวกับความขัดแย้งของเรา :) ฉันหมายถึงการยืนยันแบบ C ฉันเพิ่งสังเกตเห็นว่านี่เป็นคำถาม. NET cplusplus.com/reference/clibrary/cassert/assert
Andres Jaan Tack

5

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

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

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


3

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

  1. ใช้โรงงานวัตถุทดสอบเพื่อสร้างโครงสร้างข้อมูลอินพุตดังนั้นคุณไม่จำเป็นต้องทำซ้ำตรรกะนั้น บางทีดูในห้องสมุดผู้ช่วยเช่นการแก้ไขอัตโนมัติเพื่อลดรหัสที่จำเป็นสำหรับการตั้งค่าการทดสอบ
  2. สำหรับแต่ละคลาสการทดสอบให้สร้างการรวมศูนย์ของ SUT ซึ่งจะทำให้ง่ายต่อการเปลี่ยนแปลงเมื่อสิ่งต่าง ๆ ได้รับการปรับสภาพใหม่
  3. จำไว้ว่ารหัสทดสอบนั้นสำคัญกับรหัสการผลิต มันควรจะ refactored ถ้าคุณพบว่าคุณกำลังทำซ้ำตัวเองถ้ารหัสรู้สึก unmantainable ฯลฯ ฯลฯ

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

@driis - ถูกต้องรหัสทดสอบมีสำนวนที่แตกต่างจากการรันโค้ด การซ่อนสิ่งต่าง ๆ โดยการปรับเปลี่ยนรหัส 'ทั่วไป' และการใช้สิ่งต่าง ๆ เช่นคอนเทนเนอร์ IoC เพียงแค่ปกปิดปัญหาการออกแบบที่ถูกเปิดเผยโดยการทดสอบของคุณ
Ritch Melton

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

2

จัดการทดสอบอย่างที่คุณทำด้วยซอร์สโค้ด

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


1

แน่นอนคุณควรมีลักษณะที่เจอราร์ด Meszaros ของรูปแบบการทดสอบ xUnit มันมีส่วนที่ดีกับสูตรมากมายเพื่อนำมาใช้ใหม่รหัสการทดสอบของคุณและหลีกเลี่ยงการทำซ้ำ

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

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

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