ใน TDD มี 3 ระยะ: จัดเตรียม ดำเนินการ และยืนยัน (กำหนด เมื่อใด จากนั้นเป็น BDD) ขั้นตอนการยืนยันมีการสนับสนุนเครื่องมือที่ยอดเยี่ยม คุณอาจคุ้นเคยกับ AssertJ, FEST-Assert หรือ Hamcrest มันตรงกันข้ามกับขั้นตอนการจัดเรียง แม้ว่าการจัดเรียงข้อมูลการทดสอบมักจะเป็นเรื่องที่ท้าทาย และโดยทั่วไปแล้วส่วนสำคัญของการทดสอบจะเน้นไปที่ข้อมูลดังกล่าวโดยเฉพาะ แต่ก็ยากที่จะชี้ให้เห็นถึงเครื่องมือที่รองรับข้อมูลดังกล่าว
Test Arranger พยายามเติมเต็มช่องว่างนี้ด้วยการจัดเรียงอินสแตนซ์ของคลาสที่จำเป็นสำหรับการทดสอบ อินสแตนซ์จะเต็มไปด้วยค่าสุ่มหลอกที่ทำให้กระบวนการสร้างข้อมูลทดสอบง่ายขึ้น ผู้ทดสอบจะประกาศเฉพาะประเภทของออบเจ็กต์ที่จำเป็นและรับอินสแตนซ์ใหม่ล่าสุด เมื่อค่าสุ่มหลอกสำหรับฟิลด์ที่กำหนดไม่ดีพอ ต้องตั้งค่าฟิลด์นี้ด้วยตนเองเท่านั้น:
Product product = Arranger . some ( Product . class );
product . setBrand ( "Ocado" );
< dependency >
< groupId >com.ocadotechnology.gembus groupId >
< artifactId >test-arranger artifactId >
< version >1.6.3 version >
dependency >
testImplementation ' com.ocadotechnology.gembus:test-arranger:1.6.3 '
คลาส Arranger มีวิธีการคงที่หลายวิธีสำหรับการสร้างค่าสุ่มหลอกของประเภทธรรมดา แต่ละรายการมีฟังก์ชันการตัดคำเพื่อให้การโทรสำหรับ Kotlin ง่ายขึ้น การโทรที่เป็นไปได้บางส่วนมีดังต่อไปนี้:
ชวา | คอตลิน | ผลลัพธ์ |
---|---|---|
Arranger.some(Product.class) | some | อินสแตนซ์ของผลิตภัณฑ์ที่มีทุกฟิลด์เต็มไปด้วยค่า |
Arranger.some(Product.class, "brand") | some | ตัวอย่างของ Product ที่ไม่มีค่าสำหรับช่องแบรนด์ |
Arranger.someSimplified(Category.class) | someSimplified | อินสแตนซ์ของหมวดหมู่ ฟิลด์ของคอลเลกชันประเภทมีขนาดลดลงเหลือ 1 และความลึกสำหรับแผนผังออบเจ็กต์ถูกจำกัดไว้ที่ 3 |
Arranger.someObjects(Product.class, 7) | someObjects | สตรีมขนาด 7 ของอินสแตนซ์ของผลิตภัณฑ์ |
Arranger.someEmail() | someEmail() | สตริงที่มีที่อยู่อีเมล |
Arranger.someLong() | someLong() | ตัวเลขสุ่มหลอกประเภทยาว |
Arranger.someFrom(listOfCategories) | someFrom(listOfCategories) | รายการจาก listOfCategories |
Arranger.someText() | someText() | สตริงที่สร้างจาก Markov Chain โดยค่าเริ่มต้น มันเป็นเชนที่ง่ายมาก แต่สามารถกำหนดค่าใหม่ได้โดยการใส่ไฟล์ 'enMarkovChain' อื่น ๆ ลงใน classpath ทดสอบพร้อมคำจำกัดความทางเลือก คุณสามารถค้นหาไฟล์ที่ได้รับการฝึกในคลังข้อมูลภาษาอังกฤษได้ที่นี่ ปรึกษาไฟล์ที่รวมอยู่ในโครงการ 'enMarkovChain' สำหรับรูปแบบไฟล์ |
- | some | ตัวอย่างของผลิตภัณฑ์ที่มีช่องทั้งหมดเต็มไปด้วยค่าสุ่ม ยกเว้น name ที่ตั้งค่าเป็น "ไม่สุ่มมาก" ไวยากรณ์นี้สามารถใช้เพื่อตั้งค่าช่องของออบเจ็กต์ได้มากเท่าที่จำเป็น แต่แต่ละออบเจ็กต์จะต้องไม่แน่นอน |
ข้อมูลแบบสุ่มทั้งหมดอาจไม่เหมาะกับทุกกรณีการทดสอบ มักจะมีอย่างน้อยหนึ่งช่องที่มีความสำคัญต่อเป้าหมายการทดสอบและต้องการค่าที่แน่นอน เมื่อคลาสที่จัดไว้ไม่แน่นอน หรือเป็นคลาสข้อมูล Kotlin หรือมีวิธีสร้างสำเนาที่แก้ไข (เช่น @Builder(toBuilder = true) ของ Lombok) เพียงแค่ใช้สิ่งที่มีอยู่ โชคดีที่แม้ว่าจะไม่สามารถปรับได้ คุณสามารถใช้ Test Arranger ได้ มีเมธอด some()
และ someObjects()
เวอร์ชันเฉพาะที่ยอมรับพารามิเตอร์ประเภท Map
ปุ่มต่างๆ ในแผนผังนี้แสดงถึงชื่อฟิลด์ ในขณะที่ซัพพลายเออร์ที่เกี่ยวข้องส่งมอบค่าที่ Test Arranger จะตั้งค่าให้คุณในฟิลด์เหล่านั้น เช่น:
Product product = Arranger . some ( Product . class , Map . of ( "name" , () -> value ));
ตามค่าเริ่มต้น ค่าสุ่มจะถูกสร้างขึ้นตามประเภทฟิลด์ ค่าสุ่มไม่สอดคล้องกับค่าคงที่ของคลาสเสมอไป เมื่อจำเป็นต้องจัดเตรียมเอนทิตีเกี่ยวกับกฎบางอย่างเกี่ยวกับค่าของฟิลด์อยู่เสมอ คุณอาจจัดเตรียมผู้เรียบเรียงแบบกำหนดเอง:
class ProductArranger extends CustomArranger < Product > {
@ Override
protected Product instance () {
Product product = enhancedRandom . nextObject ( Parent . class );
product . setPrice ( BigDecimal . valueOf ( Arranger . somePositiveLong ( 9_999L )));
return product ;
}
}
เพื่อให้สามารถควบคุมกระบวนการสร้างอินสแตนซ์ Product
ได้ เราจำเป็นต้องแทนที่เมธอด instance()
ภายในวิธีการนี้เราสามารถสร้างอินสแตนซ์ของ Product
ได้ตามต้องการ โดยเฉพาะเราอาจสร้างค่าสุ่มบางอย่าง เพื่อความสะดวก เรามีฟิลด์ enhancedRandom
ในคลาส CustomArranger
ในตัวอย่างที่กำหนด เราสร้างอินสแตนซ์ของ Product
โดยทุกฟิลด์มีค่าสุ่มเทียม แต่จากนั้นเราจะเปลี่ยนราคาเป็นสิ่งที่ยอมรับได้ในโดเมนของเรา นั่นไม่เป็นลบและน้อยกว่าเลข 10k
ProductArranger
จะถูกรับโดยอัตโนมัติ (โดยใช้การสะท้อน) โดย Arranger และใช้ทุกครั้งที่มีการร้องขออินสแตนซ์ใหม่ของ Product
ไม่เพียงแต่เกี่ยวข้องกับการโทรโดยตรงเช่น Arranger.some(Product.class)
แต่ยังรวมถึงการโทรทางอ้อมด้วย สมมติว่ามีคลาส Shop
ที่มี products
ภาคสนามประเภท List
เมื่อเรียก Arranger.some(Shop.class)
Arranger จะใช้ ProductArranger
เพื่อสร้างสินค้าทั้งหมดที่จัดเก็บไว้ใน Shop.products
ลักษณะการทำงานของตัวจัดการการทดสอบสามารถกำหนดค่าได้โดยใช้คุณสมบัติ หากคุณสร้างไฟล์ arranger.properties
และบันทึกไว้ในรูทของ classpath (โดยปกติจะเป็นไดเร็กทอรี src/test/resources/
) ไฟล์นั้นจะถูกหยิบขึ้นมาและคุณสมบัติต่อไปนี้จะถูกนำไปใช้:
arranger.root
ตัวจัดเรียงแบบกำหนดเองจะถูกหยิบขึ้นมาโดยใช้การสะท้อนกลับ คลาสทั้งหมดที่ขยาย CustomArranger
จะถือเป็นตัวจัดเรียงแบบกำหนดเอง การสะท้อนจะเน้นไปที่แพ็คเกจบางอย่างซึ่งโดยค่าเริ่มต้นคือ com.ocado
นั่นไม่จำเป็นว่าจะสะดวกสำหรับคุณ อย่างไรก็ตาม ด้วย arranger.root=your_package
คุณสามารถเปลี่ยนเป็น your_package
ได้ พยายามให้แพ็คเกจมีความเฉพาะเจาะจงที่สุดเท่าที่จะเป็นไปได้ เช่น การมีบางอย่างที่เป็นแบบทั่วไป (เช่น แค่ com
ซึ่งเป็นแพ็คเกจรูทในไลบรารีจำนวนมาก) จะส่งผลให้มีการสแกนคลาสหลายร้อยคลาส ซึ่งจะต้องใช้เวลาอย่างเห็นได้ชัดarranger.randomseed
โดยค่าเริ่มต้น จะใช้เมล็ดเดียวกันเสมอในการเริ่มต้นตัวสร้างค่าสุ่มเทียมที่ซ่อนอยู่ ด้วยเหตุนี้ การดำเนินการครั้งต่อๆ ไปจะสร้างค่าเดียวกัน เพื่อให้เกิดการสุ่มระหว่างการรัน กล่าวคือ เริ่มต้นด้วยค่าสุ่มอื่นๆ เสมอ การตั้งค่า arranger.randomseed=true
เป็นสิ่งที่จำเป็นarranger.cache.enable
กระบวนการจัดเรียงอินสแตนซ์แบบสุ่มต้องใช้เวลาพอสมควร หากคุณสร้างอินสแตนซ์จำนวนมากและไม่ต้องการให้สุ่มทั้งหมด การเปิดใช้งานแคชอาจเป็นหนทางหนึ่ง เมื่อเปิดใช้งาน แคชจะจัดเก็บการอ้างอิงไปยังแต่ละอินสแตนซ์แบบสุ่ม และในบางจุดตัวจัดเตรียมการทดสอบจะหยุดสร้างอินสแตนซ์ใหม่ และนำอินสแตนซ์ที่แคชไว้กลับมาใช้แทน ตามค่าเริ่มต้น แคชจะถูกปิดใช้งานarranger.overridedefaults
Test-arranger เคารพการเริ่มต้นฟิลด์เริ่มต้น กล่าวคือ เมื่อมีฟิลด์ที่เริ่มต้นด้วยสตริงว่าง อินสแตนซ์ที่ส่งคืนโดย test-arranger จะมีสตริงว่างในฟิลด์นี้ ไม่ใช่สิ่งที่คุณต้องการในการทดสอบเสมอไป โดยเฉพาะอย่างยิ่งเมื่อมีแบบแผนในโปรเจ็กต์ให้เริ่มต้นฟิลด์ด้วยค่าว่าง โชคดีที่คุณสามารถบังคับให้ test-arranger เขียนทับค่าเริ่มต้นด้วยค่าสุ่มได้ ตั้งค่า arranger.overridedefaults
ให้เป็นจริงเพื่อแทนที่การกำหนดค่าเริ่มต้นarranger.maxRandomizationDepth
โครงสร้างข้อมูลทดสอบบางตัวสามารถสร้างสายโซ่ยาวของวัตถุที่อ้างอิงถึงกัน อย่างไรก็ตาม หากต้องการใช้ในกรณีทดสอบอย่างมีประสิทธิภาพ การควบคุมความยาวของโซ่เหล่านี้ถือเป็นสิ่งสำคัญ ตามค่าเริ่มต้น Test-arranger จะหยุดสร้างออบเจ็กต์ใหม่ที่ระดับความลึกการซ้อนระดับที่ 4 หากการตั้งค่าเริ่มต้นนี้ไม่เหมาะกับกรณีทดสอบโครงการของคุณ คุณสามารถปรับเปลี่ยนได้โดยใช้พารามิเตอร์นี้ เมื่อคุณมีบันทึก Java ที่สามารถใช้เป็นข้อมูลทดสอบได้ แต่คุณจำเป็นต้องเปลี่ยนฟิลด์หนึ่งหรือสองฟิลด์ คลาส Data
พร้อมวิธีคัดลอกจะเป็นวิธีแก้ปัญหา สิ่งนี้มีประโยชน์อย่างยิ่งเมื่อต้องรับมือกับบันทึกที่ไม่เปลี่ยนรูปแบบซึ่งไม่มีวิธีที่ชัดเจนในการเปลี่ยนแปลงฟิลด์โดยตรง
วิธี Data.copy
ช่วยให้คุณสร้างสำเนาแบบตื้นของบันทึกในขณะที่เลือกแก้ไขฟิลด์ที่ต้องการได้ ด้วยการจัดเตรียมแผนที่ของการแทนที่ฟิลด์ คุณสามารถระบุฟิลด์ที่ต้องแก้ไขและค่าใหม่ได้ วิธีการคัดลอกดูแลการสร้างอินสแตนซ์ใหม่ของเรกคอร์ดด้วยค่าฟิลด์ที่อัปเดต
วิธีการนี้ช่วยให้คุณไม่ต้องสร้างออบเจ็กต์บันทึกใหม่และตั้งค่าฟิลด์ทีละรายการด้วยตนเอง ซึ่งเป็นวิธีที่สะดวกในการสร้างข้อมูลทดสอบโดยมีการเปลี่ยนแปลงเล็กน้อยจากบันทึกที่มีอยู่
โดยรวมแล้ว คลาสข้อมูลและวิธีการคัดลอกจะช่วยสถานการณ์โดยเปิดใช้งานการสร้างสำเนาแบบตื้นของบันทึกโดยมีการเปลี่ยนแปลงฟิลด์ที่เลือก ให้ความยืดหยุ่นและความสะดวกสบายเมื่อทำงานกับประเภทบันทึกที่ไม่เปลี่ยนรูปแบบ:
Data . copy ( myRecord , Map . of ( "recordFieldName" , () -> "altered value" ));
เมื่อผ่านการทดสอบโครงการซอฟต์แวร์ แทบไม่มีใครรู้สึกว่าไม่สามารถทำได้ดีกว่านี้ ในขอบเขตของการจัดเรียงข้อมูลการทดสอบ มีสองด้านที่เราพยายามปรับปรุงด้วย Test Arranger
การทดสอบจะเข้าใจง่ายกว่ามากเมื่อทราบเจตนาของผู้สร้าง เช่น เหตุใดจึงเขียนการทดสอบ และควรตรวจพบปัญหาประเภทใด น่าเสียดายที่ไม่ใช่เรื่องแปลกที่จะเห็นการทดสอบในคำสั่งส่วนการจัดเตรียม (ที่กำหนด) ดังตัวอย่างต่อไปนี้:
Product product = Product . builder ()
. withName ( "Some name" )
. withBrand ( "Some brand" )
. withPrice ( new BigDecimal ( "12.99" ))
. withCategory ( "Water, Juice & Drinks / Juice / Fresh" )
...
. build ();
เมื่อดูโค้ดดังกล่าว เป็นการยากที่จะบอกว่าค่าใดที่เกี่ยวข้องกับการทดสอบ และค่าใดที่ให้ไว้เพื่อตอบสนองข้อกำหนดที่ไม่เป็นค่าว่างบางประการเท่านั้น หากการทดสอบเกี่ยวกับแบรนด์ ทำไมไม่เขียนแบบนั้น:
Product product = Arranger . some ( Product . class );
product . setBrand ( "Some brand" );
ตอนนี้เห็นได้ชัดว่าแบรนด์มีความสำคัญ เรามาลองก้าวไปอีกขั้นหนึ่งกันดีกว่า การทดสอบทั้งหมดอาจมีลักษณะดังนี้:
//arrange
Product product = Arranger . some ( Product . class );
product . setBrand ( "Some brand" );
//act
Report actualReport = sut . createBrandReport ( Collections . singletonList ( product ))
//assert
assertThat ( actualReport . getBrand ). isEqualTo ( "Some brand" )
เรากำลังทดสอบว่ามีการสร้างรายงานสำหรับแบรนด์ "บางยี่ห้อ" แล้ว แต่นั่นคือเป้าหมายเหรอ? เหมาะสมกว่าที่จะคาดหวังว่ารายงานจะถูกสร้างขึ้นสำหรับแบรนด์เดียวกันซึ่งมีการกำหนดผลิตภัณฑ์ที่กำหนด ดังนั้นสิ่งที่เราต้องการทดสอบคือ:
//arrange
Product product = Arranger . some ( Product . class );
//act
Report actualReport = sut . createBrandReport ( Collections . singletonList ( product ))
//assert
assertThat ( actualReport . getBrand ). isEqualTo ( product . getBrand ())
ในกรณีที่ฟิลด์ยี่ห้อไม่แน่นอนและเรากลัวว่า sut
อาจแก้ไขมัน เราสามารถเก็บค่าของมันไว้ในตัวแปรก่อนที่จะเข้าสู่ขั้นตอนการดำเนินการและนำไปใช้ในการยืนยันในภายหลัง การทดสอบจะยาวนานขึ้นแต่ความตั้งใจยังคงชัดเจน
เป็นที่น่าสังเกตว่าสิ่งที่เราเพิ่งทำไปคือการประยุกต์ใช้ Generated Value และรูปแบบวิธีการสร้างบางส่วนที่อธิบายไว้ใน xUnit Test Patterns: Refactoring Test Code โดย Gerard Meszaros
คุณเคยเปลี่ยนแปลงสิ่งเล็กๆ น้อยๆ ในโค้ดการผลิตและจบลงด้วยข้อผิดพลาดในการทดสอบหลายสิบครั้งหรือไม่? บางคนรายงานการยืนยันที่ล้มเหลว บางคนอาจถึงกับปฏิเสธที่จะคอมไพล์ นี่คือกลิ่นรหัสการผ่าตัดปืนลูกซอง ที่เพิ่งถูกยิงในการทดสอบที่ไร้เดียงสาของคุณ อาจจะไม่บริสุทธิ์นักเนื่องจากสามารถออกแบบให้แตกต่างออกไป เพื่อจำกัดความเสียหายของหลักประกันที่เกิดจากการเปลี่ยนแปลงเล็กๆ น้อยๆ ลองวิเคราะห์โดยใช้ตัวอย่าง สมมติว่าเรามีคลาสต่อไปนี้ในโดเมนของเรา:
class TimeRange {
private LocalDateTime start ;
private long durationinMs ;
public TimeRange ( LocalDateTime start , long durationInMs ) {
...
และมีการใช้ในหลายสถานที่ โดยเฉพาะอย่างยิ่งในการทดสอบ โดยไม่มี Test Arranger โดยใช้คำสั่งเช่นนี้: new TimeRange(LocalDateTime.now(), 3600_000L);
จะเกิดอะไรขึ้นหากเราถูกบังคับให้เปลี่ยนชั้นเรียนเป็น: ด้วยเหตุผลสำคัญบางประการ
class TimeRange {
private LocalDateTime start ;
private LocalDateTime end ;
public TimeRange ( LocalDateTime start , LocalDateTime end ) {
...
มันค่อนข้างท้าทายที่จะสร้างชุดของการปรับโครงสร้างใหม่ที่จะแปลงเวอร์ชันเก่าไปเป็นเวอร์ชันใหม่โดยไม่ทำลายการทดสอบที่ต้องพึ่งพาทั้งหมด มีแนวโน้มมากกว่าที่สถานการณ์จะมีการปรับการทดสอบเป็น API ใหม่ของคลาสทีละรายการ นี่หมายถึงงานที่ไม่น่าตื่นเต้นมากนักและมีคำถามมากมายเกี่ยวกับค่าระยะเวลาที่ต้องการ (ฉันควรแปลงมันเป็นจุด end
ของประเภท LocalDateTime อย่างระมัดระวัง หรือเป็นเพียงค่าสุ่มที่สะดวก) ชีวิตจะง่ายขึ้นมากด้วย Test Arranger เมื่อในทุกสถานที่ที่ไม่ต้องการ TimeRange
ที่เป็นโมฆะ เรามี Arranger.some(TimeRange.class)
มันจะดีสำหรับ TimeRange
เวอร์ชันใหม่พอๆ กับเวอร์ชันเก่า นั่นทำให้เรามีบางกรณีที่ไม่ต้องการ TimeRange
แบบสุ่ม แต่เนื่องจากเราใช้ Test Arranger เพื่อเปิดเผยความตั้งใจในการทดสอบแล้ว ในแต่ละกรณี เราก็รู้แน่ชัดว่าควรใช้ค่าใดสำหรับ TimeRange
แต่นั่นไม่ใช่ทุกสิ่งที่เราสามารถทำได้เพื่อปรับปรุงการทดสอบ สมมุติว่าเราสามารถระบุบางหมวดหมู่ของอินสแตนซ์ TimeRange
ได้ เช่น ช่วงจากอดีต ช่วงจากอนาคต และช่วงที่ใช้งานอยู่ในปัจจุบัน TimeRangeArranger
เป็นสถานที่ที่ดีเยี่ยมในการจัดเตรียม:
class TimeRangeArranger extends CustomArranger < TimeRange > {
private final long MAX_DISTANCE = 999_999L ;
@ Override
protected TimeRange instance () {
LocalDateTime start = enhancedRandom . nextObject ( LocalDateTime . class );
LocalDateTime end = start . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , end );
}
public TimeRange fromPast () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime end = now . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( end . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE )), end );
}
public TimeRange fromFuture () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime start = now . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , start . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE )));
}
public TimeRange currentlyActive () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime start = now . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
LocalDateTime end = now . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , end );
}
}
วิธีการสร้างดังกล่าวไม่ควรสร้างขึ้นล่วงหน้า แต่ควรสอดคล้องกับกรณีทดสอบที่มีอยู่ อย่างไรก็ตาม มีโอกาสที่ TimeRangeArranger
จะครอบคลุมทุกกรณีที่อินสแตนซ์ของ TimeRange
ถูกสร้างขึ้นสำหรับการทดสอบ ด้วยเหตุนี้ แทนที่จะเรียก Constructor ด้วยพารามิเตอร์ลึกลับหลายตัว เรามีตัวจัดเรียงที่มีวิธีการที่มีชื่อเสียงซึ่งอธิบายความหมายโดเมนของวัตถุที่สร้างขึ้น และช่วยในการทำความเข้าใจจุดประสงค์ในการทดสอบ
เราระบุผู้สร้างข้อมูลการทดสอบได้สองระดับเมื่อพูดคุยถึงความท้าทายที่ Test Arranger แก้ไขได้ เพื่อให้ภาพสมบูรณ์ เราต้องพูดถึงอีกอย่างน้อยหนึ่งรายการ นั่นก็คือโปรแกรมการแข่งขัน เพื่อประโยชน์ของการสนทนานี้ เราอาจถือว่า Fixture เป็นคลาสที่ออกแบบมาเพื่อสร้างโครงสร้างที่ซับซ้อนของข้อมูลการทดสอบ ตัวจัดเรียงแบบกำหนดเองจะเน้นไปที่คลาสเดียวเสมอ แต่บางครั้งคุณสามารถสังเกตได้ในกรณีทดสอบของคุณที่เกิดกลุ่มดาวของคลาสตั้งแต่สองคลาสขึ้นไปซ้ำ นั่นอาจเป็นผู้ใช้และบัญชีธนาคารของเขาหรือเธอ อาจมี CustomArranger สำหรับแต่ละรายการ แต่เหตุใดจึงเพิกเฉยต่อข้อเท็จจริงที่ว่าพวกเขามักจะมารวมกัน นี่คือเวลาที่เราควรเริ่มคิดถึงการแข่งขัน จะรับผิดชอบในการสร้างทั้งผู้ใช้และบัญชีธนาคาร (สันนิษฐานว่าใช้ตัวจัดการแบบกำหนดเองโดยเฉพาะ) และเชื่อมโยงเข้าด้วยกัน ฟิกซ์เจอร์มีการอธิบายโดยละเอียด รวมถึงรูปแบบการใช้งานต่างๆ ใน xUnit Test Patterns: Refactoring Test Code โดย Gerard Meszaros
ดังนั้นเราจึงมี Building Block สามประเภทในคลาสการทดสอบ แต่ละรายการสามารถพิจารณาได้ว่าเป็นสิ่งที่คู่กันของแนวคิด (Domain Driven Design building block) จากรหัสการผลิต:
บนพื้นผิวมีวัตถุดั้งเดิมและเรียบง่าย นั่นคือสิ่งที่ปรากฏแม้ในการทดสอบหน่วยที่ง่ายที่สุด คุณสามารถครอบคลุมการจัดเรียงข้อมูลการทดสอบดังกล่าวด้วยวิธี someXxx
จากคลาส Arranger
ดังนั้น คุณอาจมีบริการที่ต้องมีการทดสอบที่ทำงานเฉพาะกับอินสแตนซ์ User
หรือทั้ง User
และคลาสอื่นๆ ที่อยู่ในคลาส User
เช่น รายการที่อยู่ เพื่อให้ครอบคลุมกรณีดังกล่าว โดยทั่วไปจำเป็นต้องมีตัวจัดเรียงแบบกำหนดเอง เช่น UserArranger
มันจะสร้างอินสแตนซ์ของ User
ที่คำนึงถึงข้อจำกัดและค่าคงที่ของคลาสทั้งหมด ยิ่งไปกว่านั้น มันจะเลือก AddressArranger
เมื่อมีอยู่แล้ว เพื่อกรอกรายการที่อยู่ด้วยข้อมูลที่ถูกต้อง เมื่อกรณีทดสอบหลายกรณีต้องการผู้ใช้บางประเภท เช่น ผู้ใช้ไร้บ้านที่มีรายการที่อยู่ว่าง คุณสามารถสร้างวิธีการเพิ่มเติมใน UserArranger ได้ ด้วยเหตุนี้ เมื่อใดก็ตามที่จำเป็นต้องสร้างอินสแตนซ์ User
สำหรับการทดสอบ การตรวจสอบ UserArranger
และเลือกวิธีการจากโรงงานที่เหมาะสมหรือเพียงแค่เรียก Arranger.some(User.class)
ก็เพียงพอแล้ว
กรณีที่ท้าทายที่สุดเกี่ยวกับการทดสอบขึ้นอยู่กับโครงสร้างข้อมูลขนาดใหญ่ ในอีคอมเมิร์ซนั้นอาจเป็นร้านค้าที่มีสินค้ามากมาย แต่ยังมีบัญชีผู้ใช้ที่มีประวัติการซื้อด้วย การจัดเรียงข้อมูลสำหรับกรณีทดสอบดังกล่าวมักจะไม่ใช่เรื่องเล็กน้อย และการทำซ้ำสิ่งนั้นก็ไม่ฉลาด จะดีกว่ามากหากเก็บไว้ในคลาสเฉพาะภายใต้วิธีการที่มีชื่อเสียง เช่น shopWithNineProductsAndFourCustomers
และนำมาใช้ซ้ำในการทดสอบแต่ละครั้ง เราขอแนะนำอย่างยิ่งให้ใช้แบบแผนการตั้งชื่อสำหรับคลาสดังกล่าว เพื่อให้ง่ายต่อการค้นหา คำแนะนำของเราคือใช้ Fixture
postfix ในที่สุดเราอาจจะจบลงด้วยสิ่งนี้:
class ShopFixture {
Repository repo ;
public void shopWithNineProductsAndFourCustomers () {
Arranger . someObjects ( Product . class , 9 )
. forEach ( p -> repo . save ( p ));
Arranger . someObjects ( Customer . class , 4 )
. forEach ( p -> repo . save ( p ));
}
}
เวอร์ชัน test-arranger ใหม่ล่าสุดได้รับการคอมไพล์โดยใช้ Java 17 และควรใช้ในรันไทม์ Java 17+ อย่างไรก็ตาม ยังมีสาขา Java 8 สำหรับความเข้ากันได้แบบย้อนหลัง ซึ่งครอบคลุมในเวอร์ชัน 1.4.x