การออกแบบการทดสอบหน่วยสำหรับระบบ stateful


20

พื้นหลัง

การทดสอบพัฒนาการขับเคลื่อนได้รับความนิยมหลังจากเรียนจบแล้วและในอุตสาหกรรม ฉันพยายามที่จะเรียนรู้ แต่สิ่งสำคัญบางอย่างยังคงหลบหนีฉัน ผู้เสนอ TDD พูดหลายสิ่งหลายอย่างเช่น (ต่อไปนี้เรียกว่า "หลักการยืนยันเดียว" หรือSAP ):

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

ที่มา: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

พวกเขายังพูดแบบนี้ (ซึ่งต่อไปจะเรียกว่า "หลักการวิธีการส่วนตัว" หรือPMP ):

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

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

ที่มา: คุณทำการทดสอบวิธีการส่วนตัวอย่างไร

สถานการณ์

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

  • SAP แนะนำว่าฉันไม่ควรทดสอบ "ขั้นตอนการสร้างรัฐ" ฉันควรสมมติว่าสถานะเป็นสิ่งที่ฉันคาดหวังจากรหัสการสะสมแล้วทดสอบการเปลี่ยนสถานะเดียวที่ฉันพยายามทดสอบ

  • PMP แนะนำว่าฉันไม่สามารถข้ามขั้นตอน "state build up" นี้ได้และเพียงทดสอบวิธีการที่ควบคุมการทำงานนั้นอย่างอิสระ

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


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


@Doval: โปรดอธิบายวิธีทำบางสิ่งบางอย่างเช่นโทรศัพท์ (SIP UserAgent) ที่ไม่ใช่แบบรัฐ พฤติกรรมที่คาดหวังของหน่วยนี้ระบุไว้ใน RFC โดยใช้แผนภาพการเปลี่ยนสถานะ
Bart van Ingen Schenau

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

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

คำตอบ:


15

มุมมอง:

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

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

หลักการ - SAP:

ในขณะที่ฉันไม่ใช่ผู้เชี่ยวชาญใน TDD ฉันคิดว่าคุณขาดส่วนหนึ่งของ Single Assertion Principle (SAP) ที่พยายามจะสอน SAP สามารถปรับปรุงใหม่เป็น "ทดสอบครั้งละหนึ่งสิ่ง" แต่ TOTAT ไม่ได้ใช้ภาษาอย่างง่ายเช่นเดียวกับ SAP

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

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

หลักการ - PMP:

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

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


ใช้ TDD ( สำหรับคุณ )

ดังนั้นสถานการณ์ของคุณจะมีรอยย่นเล็กน้อยเกินกว่าแอปพลิเคชันทั่วไป วิธีการของแอปของคุณมีสถานะดังนั้นการส่งออกของพวกเขาจะเกิดขึ้นไม่เพียง แต่การป้อนข้อมูล แต่ยังมีสิ่งที่ทำมาก่อน ฉันแน่ใจว่าฉันควร<insert some lecture>ที่นี่เกี่ยวกับสถานะที่น่ากลัวและ blah blah blah แต่นั่นไม่ได้ช่วยแก้ปัญหาของคุณ

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

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

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

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

ก่อนอื่นคุณต้องยอมรับว่าคุณใช้บางสิ่งบางอย่างเป็นการทดสอบในสถานการณ์หนึ่งและการตั้งค่าในสถานการณ์อื่น ในแง่หนึ่งดูเหมือนว่านี่เป็นการละเมิดโดยตรงของ SAP แต่ถ้าคุณใช้เหตุผลเชิงตรรกะChangeFooToState1ว่ามีจุดประสงค์สองประการคุณก็ยังคงพบกับจิตวิญญาณของสิ่งที่ SAP สอนเรา เมื่อคุณต้องการให้แน่ใจว่าFoo()สถานะการเปลี่ยนแปลงจากนั้นคุณใช้ChangeFooToState1เป็นแบบทดสอบ และเมื่อต้องการตรวจสอบFoo()ผลลัพธ์ของState1" เมื่ออยู่ใน" คุณจะใช้ChangeFooToState1เป็นค่าติดตั้ง

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

วางไว้ด้วยกัน:

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

ถ้าคุณทำตามวิธีการนั้นการbloated, complicated, long, and difficult to writeทดสอบควรจะง่ายขึ้นเล็กน้อยในการจัดการ โดยทั่วไปแล้วควรจบลงให้เล็กลงและกระชับขึ้น (เช่นซับซ้อนน้อยกว่า) คุณควรสังเกตว่าการทดสอบนั้นแยกกันหรือแยกส่วนมากขึ้น

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


11

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

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

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

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

คุณไม่สามารถหลีกเลี่ยงสถานะทั้งหมด แต่คุณสามารถย่อเล็กสุดและแยกออกได้


เพียงเพื่อบันทึกตัวอย่าง SIP เป็นของฉันไม่ใช่จาก OP และเครื่องรัฐบางเครื่องอาจต้องใช้วิธีการเรียกมากกว่าสองสามวิธีเพื่อให้เครื่องอยู่ในสถานะที่ถูกต้องสำหรับการทดสอบ
Bart van Ingen Schenau

+1 สำหรับ "คุณไม่สามารถหลีกเลี่ยงสถานะทั้งหมด แต่คุณสามารถย่อขนาดและแยกได้" ฉันไม่เห็นด้วย รัฐเป็นสิ่งชั่วร้ายที่จำเป็นในซอฟต์แวร์
Brandon

0

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

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

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

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