เขียนการทดสอบที่สำคัญ: จัดการกับโค้ดที่ซับซ้อนที่สุดก่อน

เผยแพร่แล้ว: 2022-03-11

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

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

ข้อต่อและความซับซ้อนของวัฏจักร: เมตริกเพื่อความครอบคลุมการทดสอบอย่างชาญฉลาด

ลืมความคุ้มครอง 100% ทดสอบอย่างชาญฉลาดยิ่งขึ้นโดยระบุคลาสที่มีแนวโน้มที่จะพัง
ทวีต

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

แนวคิดหลักในที่นี้คือการวิเคราะห์เมตริกสองแบบ – การมีเพศสัมพันธ์ (เช่น การมีเพศสัมพันธ์ทางอวัยวะ หรือ CA) และความซับซ้อน (เช่น ความซับซ้อนของวงจร)

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

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

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

มีข้อแม้ประการหนึ่ง: สมมติว่าเรามีสองคลาส – หนึ่งมี CA 100 และความซับซ้อน 2 และอีกอันหนึ่งมี CA 60 และความซับซ้อน 20 แม้ว่าผลรวมของตัวชี้วัดจะสูงกว่าสำหรับอันแรก เราควรครอบคลุมอย่างแน่นอน คนที่สองก่อน เนื่องจากชั้นหนึ่งถูกใช้โดยชั้นอื่นๆ มากมาย แต่ไม่ซับซ้อนมาก ในทางกลับกัน คลาสที่สองก็ถูกใช้โดยคลาสอื่นๆ มากมาย แต่ค่อนข้างซับซ้อนกว่าคลาสแรก

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

โดยทั่วไป คลาสที่มีความแตกต่างน้อยที่สุดระหว่างเมตริกทั้งสองควรได้รับความสำคัญสูงสุดสำหรับการทดสอบที่ครอบคลุม

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

  • เมตริกข้อต่อ: www.spinellis.gr/sw/ckjm/
  • ความซับซ้อน: cyvis.sourceforge.net/

คณิตศาสตร์เล็กน้อย

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

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

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

f(CA, ความซับซ้อน) = A×CA + B×Complexity

โดยที่ A และ B เป็นสัมประสิทธิ์บางอย่าง

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

R1 = A∗CA + B∗ความซับซ้อน และ R2 = A∗CA + B∗ความซับซ้อน

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

ขออภัย มีปัญหากับแนวทางนี้ สำหรับบรรทัดใด ๆ (ค่าอันดับ) เราจะมีคะแนนที่มี CA ขนาดเล็กมากและความซับซ้อนที่ใหญ่มาก (และในทางกลับกันด้วยวีซ่า) วางอยู่บนนั้น ซึ่งจะทำให้คะแนนมีความแตกต่างกันมากระหว่างค่าเมตริกที่ด้านบนของรายการ ซึ่งเป็นสิ่งที่เราต้องการหลีกเลี่ยง

อีกวิธีหนึ่งในการทำสเกลาไรซ์นั้นขึ้นอยู่กับจุดอ้างอิง จุดอ้างอิงคือจุดที่มีค่าสูงสุดของเกณฑ์ทั้งสอง:

(สูงสุด (CA), สูงสุด (ความซับซ้อน))

ฟังก์ชันฟิตเนสจะเป็นระยะห่างระหว่างจุดอ้างอิงและจุดข้อมูล:

f(CA,ความซับซ้อน) = √((CA−CA ) 2 + (ความซับซ้อน−ความซับซ้อน) 2 )

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

แนวทางนี้ใช้กับค่าสุดขั้วได้ดีกว่า แต่ยังมีประเด็นอยู่สองประเด็น: อย่างแรก ฉันต้องการให้มีคะแนนมากขึ้นใกล้กับจุดอ้างอิงเพื่อเอาชนะปัญหาที่เราพบด้วยชุดค่าผสมเชิงเส้นได้ดียิ่งขึ้น ประการที่สอง – ความซับซ้อนของ CA และ Cyclomatic นั้นแตกต่างกันโดยเนื้อแท้และมีการตั้งค่าที่แตกต่างกัน ดังนั้นเราจึงจำเป็นต้องทำให้เป็นมาตรฐาน (เช่น เพื่อให้ค่าทั้งหมดของตัวชี้วัดทั้งสองมีค่าตั้งแต่ 1 ถึง 100)

นี่เป็นเคล็ดลับเล็กๆ น้อยๆ ที่เราสามารถนำมาใช้แก้ปัญหาแรกได้ แทนที่จะดูที่ CA และ Cyclomatic Complexity เราสามารถดูค่าที่กลับกัน จุดอ้างอิงในกรณีนี้จะเป็น (0,0) ในการแก้ปัญหาที่สอง เราสามารถตั้งค่าเมตริกให้เป็นมาตรฐานโดยใช้ค่าต่ำสุดได้ นี่คือลักษณะ:

ความซับซ้อนแบบกลับด้านและทำให้เป็นมาตรฐาน – NormComplexity:

(1 + นาที(ความซับซ้อน)) / (1 + ความซับซ้อน)∗100

CA แบบกลับด้านและทำให้เป็นมาตรฐาน – NormCA:

(1 + นาที(CA)) / (1+CA)∗100

หมายเหตุ: ฉันเพิ่ม 1 เพื่อให้แน่ใจว่าไม่มีการหารด้วย 0

รูปภาพต่อไปนี้แสดงพล็อตที่มีค่ากลับด้าน:

อันดับสุดท้าย

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

อันดับ(NormComplexity , NormCA) = 100 − √(NormComplexity 2 + NormCA 2 ) / √2

สถิติเพิ่มเติม

มีอีกความคิดหนึ่งที่ผมอยากจะเพิ่ม แต่มาดูสถิติกันก่อน นี่คือฮิสโตแกรมของเมตริกการมีเพศสัมพันธ์:

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

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

ข้อมูลที่เป็นประโยชน์เพิ่มเติมที่เราได้จากฮิสโตแกรมด้านบนคือเราสามารถกรองคลาสที่มีคัปปลิ้งต่ำ (CA ใน {0,1}) ออกจากรายการคลาสที่มีสิทธิ์ครอบคลุมด้วยการทดสอบหน่วย แม้ว่าชั้นเรียนเดียวกันจะเป็นตัวเลือกที่ดีสำหรับการทดสอบการรวม / การทำงาน

คุณสามารถค้นหาสคริปต์และทรัพยากรทั้งหมดที่ฉันใช้ในที่เก็บ GitHub นี้: ashalitkin/code-base-stats

มันใช้งานได้เสมอหรือไม่?

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

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

ที่เกี่ยวข้อง: หลักการความรับผิดชอบเดียว: สูตรสำหรับรหัสที่ยอดเยี่ยม