การเขียนโค้ดที่ทดสอบได้
เผยแพร่แล้ว: 2022-11-03การทดสอบหน่วยเป็นเครื่องมือสำคัญในกล่องเครื่องมือของนักพัฒนาซอฟต์แวร์ทุกคน การทดสอบหน่วยการเขียนนั้นค่อนข้างง่ายเมื่อต้องจัดการกับ codebase ที่เป็นไปตามแนวทางปฏิบัติ รูปแบบ และหลักการออกแบบซอฟต์แวร์ที่ดีที่สุด ปัญหาที่แท้จริงเกิดขึ้นเมื่อพยายามทดสอบหน่วยที่ออกแบบมาไม่ดี รหัสที่ไม่สามารถทดสอบได้
บล็อกนี้จะกล่าวถึงวิธีเขียนโค้ดที่ทดสอบได้และรูปแบบและแนวทางปฏิบัติที่ไม่ถูกต้องที่ควรหลีกเลี่ยงเพื่อปรับปรุงความสามารถในการทดสอบ
รหัสที่ทดสอบได้และทดสอบไม่ได้
เมื่อทำงานกับแอปพลิเคชันขนาดใหญ่ที่ต้องบำรุงรักษาในระยะยาว เราต้องอาศัยการทดสอบอัตโนมัติเพื่อรักษาคุณภาพโดยรวมของระบบให้อยู่ในระดับสูง เมื่อเทียบกับการทดสอบการรวม ซึ่งคุณทดสอบหลายหน่วยโดยรวม การทดสอบหน่วยมีประโยชน์ในด้านความรวดเร็วและความเสถียร รวดเร็วเพราะว่าเรากำลังสร้างอินสแตนซ์ ตามหลักการแล้ว เป็นเพียงคลาสที่ทดสอบ และเสถียรเพราะเรามักจะล้อเลียนการพึ่งพาภายนอก เช่น การเชื่อมต่อฐานข้อมูลหรือเครือข่าย
หากคุณไม่คุ้นเคยกับความแตกต่างที่แน่นอนระหว่างการทดสอบหน่วยและการทดสอบการรวม คุณสามารถอ่านเพิ่มเติมเกี่ยวกับหัวข้อนี้ได้ในบล็อกบทนำสู่การทดสอบของเรา
โค้ดที่ทดสอบได้สามารถแยกได้จากโค้ดเบสที่เหลือของเรา กล่าวอีกนัยหนึ่งหน่วยที่เล็กที่สุดสามารถทดสอบได้อย่างอิสระ โค้ดที่ทดสอบไม่ได้ถูกเขียนในลักษณะที่ยากหรือเป็นไปไม่ได้เลยที่จะเขียนการทดสอบหน่วยที่ดี
มาทบทวนรูปแบบการต่อต้านและแนวทางปฏิบัติที่ไม่ดีที่เราควรหลีกเลี่ยงเมื่อเขียนโค้ดที่ทดสอบได้
ตัวอย่างเขียนด้วยภาษาจาวา แต่รูปแบบการเขียนโค้ดที่กล่าวถึงในที่นี้ใช้กับภาษาการเขียนโปรแกรมเชิงวัตถุและเฟรมเวิร์กการทดสอบ เราจะใช้ assertj และ JUnit5 เพื่อเป็นตัวอย่างในบล็อกโพสต์นี้
การฉีดพึ่งพา
การพึ่งพาอาศัยกันเป็นหนึ่งในรูปแบบการออกแบบที่สำคัญที่สุดสำหรับการแยกการทดสอบ การฉีดการพึ่งพาเป็นรูปแบบการออกแบบที่วัตถุหนึ่งได้รับวัตถุอื่น (การพึ่งพา) ผ่านพารามิเตอร์ตัวสร้างหรือตัวตั้งค่าแทนที่จะต้องสร้างเอง
ด้วยการฉีดการพึ่งพา เราสามารถแยกคลาสภายใต้การทดสอบได้อย่างง่ายดายโดยการเยาะเย้ยการพึ่งพาของวัตถุ
มาดูตัวอย่างโดยไม่ต้องพึ่งพาการฉีด:

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

ตอนนี้ เราสามารถสร้างการใช้งานเครื่องยนต์ได้หลายแบบ และด้วยเหตุนี้ รถยนต์ที่มีเครื่องยนต์ต่างกัน:

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

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

โดยพื้นฐานแล้ว วิธีนี้จะอ่านวันที่และเวลาของระบบปัจจุบัน และส่งกลับผลลัพธ์ตามค่านั้น มันจะยากมากที่จะเขียนการทดสอบหน่วยตามสถานะที่เหมาะสมสำหรับวิธีนี้เนื่องจากการเรียกสแตติก LocalDateTime.now() จะให้ผลลัพธ์ที่แตกต่างกันระหว่างการดำเนินการทดสอบของเรา การเขียนการทดสอบสำหรับวิธีนี้เป็นไปไม่ได้หากไม่ได้เปลี่ยนวันที่และเวลาของระบบ
ในการแก้ไขปัญหานี้ เราจะส่งเมธอด date time ถึง timeOfDay เป็นอาร์กิวเมนต์:


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

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

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

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

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

UserService มีหน้าที่มากกว่าหนึ่งอย่าง – การลงทะเบียนผู้ใช้ใหม่และส่งอีเมล ขณะทดสอบการลงทะเบียนผู้ใช้ เราต้องจัดการกับบริการอีเมลและในทางกลับกัน:

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