การทดสอบซอฟต์แวร์อัตโนมัติมีความสำคัญอย่างยิ่งต่อคุณภาพในระยะยาวความสามารถในการบำรุงรักษาและความสามารถในการขยายโครงการซอฟต์แวร์และสำหรับ Java JUnit เป็นเส้นทางสู่ระบบอัตโนมัติ
ในขณะที่บทความนี้ส่วนใหญ่จะเน้นไปที่การเขียนการทดสอบหน่วยที่มีประสิทธิภาพและการใช้การทดสอบการลอกเลียนแบบและการพึ่งพา แต่เราจะพูดถึงการทดสอบ JUnit และการรวม
กรอบการทดสอบ JUnit เป็นเครื่องมือทั่วไปฟรีและโอเพ่นซอร์สสำหรับการทดสอบโครงการที่ใช้ Java
ในขณะที่เขียนนี้ JUnit 4 เป็นรุ่นหลักในปัจจุบันซึ่งได้รับการเผยแพร่เมื่อกว่า 10 ปีที่แล้วโดยการอัปเดตครั้งล่าสุดมีมานานกว่าสองปีแล้ว
JUnit 5 (ด้วยโมเดลการเขียนโปรแกรม Jupiter และส่วนขยาย) อยู่ในระหว่างการพัฒนา รองรับคุณสมบัติภาษาที่แนะนำใน จาวา 8 และรวมถึงคุณสมบัติใหม่ที่น่าสนใจอื่น ๆ บางทีมอาจพบว่า JUnit 5 พร้อมใช้งานในขณะที่บางทีมอาจใช้ JUnit 4 ต่อไปจนกว่า 5 จะวางจำหน่ายอย่างเป็นทางการ เราจะดูตัวอย่างจากทั้งสองอย่าง
การทดสอบ JUnit สามารถรันได้โดยตรงใน IntelliJ แต่ยังสามารถรันใน IDE อื่น ๆ เช่น Eclipse, NetBeans หรือแม้แต่บรรทัดคำสั่ง
การทดสอบควรรันในเวลาสร้างเสมอโดยเฉพาะการทดสอบหน่วย การสร้างที่มีการทดสอบที่ล้มเหลวควรถือว่าล้มเหลวไม่ว่าปัญหาจะอยู่ในขั้นตอนการผลิตหรือรหัสการทดสอบก็ตาม - สิ่งนี้ต้องการวินัยจากทีมงานและความเต็มใจที่จะให้ความสำคัญสูงสุดในการแก้ไขการทดสอบที่ล้มเหลว แต่จำเป็นต้องปฏิบัติตาม จิตวิญญาณของระบบอัตโนมัติ
การทดสอบ JUnit ยังสามารถรันและรายงานโดยระบบการผสานรวมแบบต่อเนื่องเช่น Jenkins โครงการที่ใช้เครื่องมือเช่น Gradle, Maven หรือ Ant มีข้อได้เปรียบเพิ่มเติมคือสามารถเรียกใช้การทดสอบโดยเป็นส่วนหนึ่งของกระบวนการสร้าง
เป็นตัวอย่างโครงการ Gradle สำหรับ JUnit 5 โปรดดูไฟล์ ส่วน Gradle ของคู่มือผู้ใช้ JUnit และ junit5-samples.git ที่เก็บ โปรดทราบว่ายังสามารถเรียกใช้การทดสอบที่ใช้ JUnit 4 API (เรียกว่า “ วินเทจ” ).
สามารถสร้างโปรเจ็กต์ใน IntelliJ ผ่านตัวเลือกเมนู File> Open …> ไปที่ junit-gradle-consumer sub-directory
> ตกลง> เปิดเป็นโครงการ> ตกลงเพื่อนำเข้าโครงการจาก Gradle
สำหรับ Eclipse ไฟล์ ปลั๊กอิน Buildship Gradle สามารถติดตั้งได้จาก Help> Eclipse Marketplace …จากนั้นสามารถอิมพอร์ตโปรเจ็กต์ด้วย File> Import …> Gradle> Gradle Project> Next> Next> เรียกดู junit-gradle-consumer
ไดเรกทอรีย่อย> ถัดไป> ถัดไป> เสร็จสิ้น
หลังจากตั้งค่าโปรเจ็กต์ Gradle ใน IntelliJ หรือ Eclipse แล้วให้รัน Gradle build
งานจะรวมการเรียกใช้การทดสอบ JUnit ทั้งหมดด้วย test
งาน. โปรดทราบว่าการทดสอบอาจถูกข้ามไปในการดำเนินการตามมาของ build
หากไม่มีการเปลี่ยนแปลงรหัส
สำหรับ JUnit 4 โปรดดู JUnit’s ใช้กับ Gradle wiki .
สำหรับ JUnit 5 โปรดดูที่ไฟล์ ส่วน Maven ของคู่มือผู้ใช้ และ junit5-samples.git ที่เก็บตัวอย่างของโครงการ Maven นอกจากนี้ยังสามารถเรียกใช้การทดสอบแบบโบราณ (การทดสอบที่ใช้ JUnit 4 API)
ใน IntelliJ ให้ใช้ไฟล์> เปิด ... > ไปที่ junit-maven-consumer/pom.xml
> ตกลง> เปิดเป็นโครงการ จากนั้นสามารถรันการทดสอบได้จาก Maven Projects> junit5-maven-consumer> Lifecycle> Test
ใน Eclipse ให้ใช้ไฟล์> นำเข้า…> Maven> โครงการ Maven ที่มีอยู่> ถัดไป> เรียกดู junit-maven-consumer
ไดเรกทอรี> ด้วย pom.xml
เลือก> เสร็จสิ้น
การทดสอบสามารถดำเนินการได้โดยรันโปรเจ็กต์เป็น Maven build …> ระบุเป้าหมายของ test
> เรียกใช้
สำหรับ JUnit 4 โปรดดู JUnit ในที่เก็บ Maven .
นอกเหนือจากการเรียกใช้การทดสอบผ่านเครื่องมือสร้างเช่น Gradle หรือ Maven แล้ว IDE จำนวนมากยังสามารถเรียกใช้การทดสอบ JUnit ได้โดยตรง
IntelliJ IDEA ต้องใช้ 2016.2 หรือใหม่กว่าสำหรับการทดสอบ JUnit 5 ในขณะที่การทดสอบ JUnit 4 ควรทำงานใน IntelliJ เวอร์ชันเก่า
สำหรับวัตถุประสงค์ของบทความนี้คุณอาจต้องการสร้างโครงการใหม่ใน IntelliJ จากที่เก็บ GitHub ของฉัน ( JUnit5IntelliJ.git หรือ JUnit4IntelliJ.git ) ซึ่งรวมไฟล์ทั้งหมดไว้ในรูปแบบง่ายๆ Person
ตัวอย่างคลาสและใช้ไลบรารี JUnit ในตัว การทดสอบสามารถรันได้ด้วย Run> Run 'All Tests' การทดสอบยังสามารถรันใน IntelliJ จาก PersonTest
ชั้นเรียน
ที่เก็บเหล่านี้ถูกสร้างขึ้นด้วยโปรเจ็กต์ IntelliJ Java ใหม่และสร้างโครงสร้างไดเร็กทอรี src/main/java/com/example
และ src/test/java/com/example
. src/main/java
ไดเร็กทอรีถูกระบุเป็นโฟลเดอร์ต้นทางในขณะที่ src/test/java
ถูกระบุเป็นโฟลเดอร์ซอร์สทดสอบ หลังจากสร้าง PersonTest
คลาสด้วยวิธีการทดสอบที่ใส่คำอธิบายประกอบด้วย @Test
อาจล้มเหลวในการคอมไพล์ซึ่งในกรณีนี้ IntelliJ เสนอคำแนะนำในการเพิ่ม JUnit 4 หรือ JUnit 5 ลงในพา ธ คลาสซึ่งสามารถโหลดได้จากการแจกแจง IntelliJ IDEA (ดู คำตอบเหล่านี้ ใน Stack Overflow สำหรับรายละเอียดเพิ่มเติม) สุดท้ายมีการเพิ่มการกำหนดค่าการรัน JUnit สำหรับการทดสอบทั้งหมด
หลักการขององค์ประกอบและการออกแบบ
ดูไฟล์ แนวทางการทดสอบ IntelliJ .
ว่างเปล่า Java โปรเจ็กต์ใน Eclipse จะไม่มีไดเร็กทอรีรูททดสอบ สิ่งนี้ถูกเพิ่มจากคุณสมบัติโปรเจ็กต์> Java Build Path> เพิ่มโฟลเดอร์…> สร้างโฟลเดอร์ใหม่…> ระบุชื่อโฟลเดอร์> เสร็จสิ้น ไดเร็กทอรีใหม่จะถูกเลือกเป็นโฟลเดอร์ต้นทาง คลิกตกลงในกล่องโต้ตอบที่เหลือทั้งสอง
สามารถสร้างการทดสอบ JUnit 4 ด้วย File> New> JUnit Test Case เลือก“ New JUnit 4 test” และซอร์สโฟลเดอร์ที่สร้างขึ้นใหม่สำหรับการทดสอบ ระบุ 'คลาสที่ทดสอบ' และ 'แพ็กเกจ' เพื่อให้แน่ใจว่าแพ็กเกจนั้นตรงกับคลาสที่กำลังทดสอบ จากนั้นระบุชื่อสำหรับคลาสทดสอบ หลังจากเสร็จสิ้นวิซาร์ดหากได้รับแจ้งให้เลือก“ เพิ่มไลบรารี JUnit 4” ไปยังเส้นทางการสร้าง จากนั้นโปรเจ็กต์หรือคลาสทดสอบแต่ละคลาสสามารถรันเป็นการทดสอบ JUnit ดูสิ่งนี้ด้วย Eclipse การเขียนและการรันการทดสอบ JUnit .
อัตรารายชั่วโมงของผู้รับเหมาต่อเงินเดือน
NetBeans รองรับการทดสอบ JUnit 4 เท่านั้น สามารถสร้างคลาสทดสอบในโปรเจ็กต์ NetBeans Java ด้วย File> New File …> Unit Tests> JUnit Test หรือ Test for Existing Class ตามค่าเริ่มต้นไดเร็กทอรีรูททดสอบจะมีชื่อว่า test
ในไดเรกทอรีโครงการ
มาดูตัวอย่างโค้ดการผลิตและโค้ดทดสอบหน่วยที่เกี่ยวข้องกันง่ายๆสำหรับ Person
ชั้นเรียน คุณสามารถดาวน์โหลดโค้ดตัวอย่างจากไฟล์ โครงการ github และเปิดผ่าน IntelliJ
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ', ' + givenName; } }
ไม่เปลี่ยนรูป Person
คลาสมีตัวสร้างและ getDisplayName()
วิธี. เราต้องการทดสอบว่า getDisplayName()
ส่งคืนชื่อที่จัดรูปแบบตามที่เราคาดหวัง นี่คือรหัสทดสอบสำหรับการทดสอบหน่วยเดียว (JUnit 5):
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person('Josh', 'Hayden'); String displayName = person.getDisplayName(); assertEquals('Hayden, Josh', displayName); } }
PersonTest
ใช้ JUnit 5’s @Test
และการยืนยัน สำหรับ JUnit 4, the PersonTest
คลาสและวิธีการต้องเป็นแบบสาธารณะและควรใช้การนำเข้าที่แตกต่างกัน นี่คือไฟล์ JUnit 4 ตัวอย่าง Gist .
เมื่อเรียกใช้ PersonTest
คลาสใน IntelliJ การทดสอบผ่านและตัวบ่งชี้ UI เป็นสีเขียว
แม้ว่าจะไม่จำเป็น แต่เราก็ใช้หลักการทั่วไปในการตั้งชื่อคลาสทดสอบ โดยเฉพาะเราเริ่มต้นด้วยชื่อของชั้นเรียนที่กำลังทดสอบ (Person
) และต่อท้าย“ การทดสอบ” (PersonTest
) การตั้งชื่อวิธีการทดสอบจะคล้ายกันโดยเริ่มจากวิธีที่กำลังทดสอบ (getDisplayName()
) และ 'ทดสอบ' ไว้ล่วงหน้า (testGetDisplayName()
) แม้ว่าจะมีหลักเกณฑ์อื่น ๆ อีกมากมายที่ยอมรับได้สำหรับวิธีการทดสอบ แต่สิ่งสำคัญคือต้องมีความสอดคล้องกันทั้งทีมและโครงการ
ชื่อในการผลิต | ชื่อในการทดสอบ |
---|---|
บุคคล | การทดสอบบุคคล |
getDisplayName() | testDisplayName() |
นอกจากนี้เรายังใช้หลักการสร้างรหัสทดสอบ PersonTest
คลาสในแพ็คเกจเดียวกัน (com.example
) กับรหัสการผลิตของ Person
ชั้นเรียน หากเราใช้แพ็กเกจอื่นสำหรับการทดสอบเราจะต้องใช้แบบสาธารณะ เข้าถึงแก้ไข ในคลาสรหัสการผลิตตัวสร้างและวิธีการที่อ้างอิงโดยการทดสอบหน่วยแม้ว่าจะไม่เหมาะสมก็ตามดังนั้นจึงควรเก็บไว้ในแพ็คเกจเดียวกันจะดีกว่า อย่างไรก็ตามเราใช้ไดเรกทอรีซอร์สแยกต่างหาก (src/main/java
และ src/test/java
) เนื่องจากโดยทั่วไปเราไม่ต้องการรวมโค้ดทดสอบในบิลด์การผลิตที่นำออกใช้
@Test
คำอธิบายประกอบ (JUnit 4 / 5 ) บอกให้ JUnit ดำเนินการ testGetDisplayName()
method เป็นวิธีทดสอบและรายงานว่าผ่านหรือไม่ผ่าน ตราบใดที่การยืนยันทั้งหมด (ถ้ามี) ผ่านและไม่มีข้อยกเว้นใด ๆ เกิดขึ้นการทดสอบจะถือว่าผ่าน
รหัสทดสอบของเราเป็นไปตามรูปแบบโครงสร้างของ จัด - กระทำ - ยืนยัน (AAA) . รูปแบบทั่วไปอื่น ๆ ได้แก่ Given-When-Then และ Setup-Exercise-Verify-Teardown (โดยทั่วไป Teardown ไม่จำเป็นต้องใช้อย่างชัดเจนสำหรับการทดสอบหน่วย) แต่เราใช้ AAA ในบทความนี้
มาดูกันว่าตัวอย่างการทดสอบของเราเป็นไปตาม AAA อย่างไร บรรทัดแรก 'จัดเรียง' จะสร้าง Person
วัตถุที่จะทดสอบ:
Person person = new Person('Josh', 'Hayden');
บรรทัดที่สอง 'การกระทำ' การออกกำลังกาย รหัสการผลิต Person.getDisplayName()
วิธี:
String displayName = person.getDisplayName();
บรรทัดที่สาม 'ยืนยัน' จะตรวจสอบว่าผลลัพธ์เป็นไปตามที่คาดไว้
assertEquals('Hayden, Josh', displayName);
ภายในนั้น assertEquals()
การโทรใช้วิธีการเท่ากับ 'Hayden, Josh' String object เพื่อตรวจสอบค่าจริงที่ส่งคืนจากรหัสการผลิต (displayName
) ที่ตรงกัน หากไม่ตรงกันแสดงว่าการทดสอบนั้นล้มเหลว
โปรดทราบว่าการทดสอบมักมีมากกว่าหนึ่งบรรทัดสำหรับแต่ละขั้นตอน AAA เหล่านี้
ตอนนี้เราได้พูดถึงรูปแบบการทดสอบบางส่วนแล้วเรามาสนใจการทำให้โค้ดการผลิตสามารถทดสอบได้
เรากลับไปที่ Person
ของเรา ชั้นเรียนซึ่งฉันได้ใช้วิธีการคืนอายุของบุคคลตามวันเกิดของเขาหรือเธอ ตัวอย่างโค้ดต้องการ Java 8 เพื่อใช้ประโยชน์จากวันที่ใหม่และ API ที่ใช้งานได้ นี่คือสิ่งใหม่ Person.java
ชั้นเรียนดูเหมือนว่า:
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); System.out.println(person.getDisplayName() + ': ' + person.getAge() + ' years'); // Doe, Joey: 4 years } }
ดำเนินการชั้นเรียนนี้ (ในขณะที่เขียน) ประกาศว่าโจอี้อายุ 4 ปี เพิ่มวิธีการทดสอบ:
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); long age = person.getAge(); assertEquals(4, age); } }
มันผ่านไปในวันนี้ แต่ถ้าเราดำเนินการในอีก 1 ปีนับจากนี้ล่ะ? การทดสอบนี้ไม่สามารถกำหนดได้และมีความเปราะเนื่องจากผลลัพธ์ที่คาดหวังขึ้นอยู่กับวันที่ปัจจุบันของระบบที่ทำการทดสอบ
เมื่อดำเนินการผลิตเราต้องการใช้วันที่ปัจจุบัน LocalDate.now()
ในการคำนวณอายุของบุคคล แต่ในการทำการทดสอบเชิงกำหนดแม้ในหนึ่งปีนับจากนี้การทดสอบจำเป็นต้องจัดหา currentDate
ของตนเอง ค่า
สิ่งนี้เรียกว่าการฉีดแบบพึ่งพา เราไม่ต้องการ Person
ของเรา วัตถุเพื่อกำหนดวันที่ปัจจุบัน แต่เราต้องการส่งผ่านตรรกะนี้เป็นการอ้างอิงแทน การทดสอบหน่วยจะใช้ค่าที่ทราบไม่ได้ขีด จำกัด และรหัสการผลิตจะอนุญาตให้ระบบจัดเตรียมค่าจริงขณะรันไทม์
มาเพิ่ม LocalDate
ผู้จัดหาให้ Person.java
:
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); System.out.println(person.getDisplayName() + ': ' + person.getAge() + ' years'); // Doe, Joey: 4 years } }
เพื่อให้ง่ายต่อการทดสอบ getAge()
เราเปลี่ยนวิธีใช้ currentDateSupplier
, a LocalDate
ซัพพลายเออร์สำหรับการดึงวันที่ปัจจุบัน หากคุณไม่ทราบว่าซัพพลายเออร์คืออะไรขอแนะนำให้อ่านเกี่ยวกับ Lambda อินเทอร์เฟซการทำงานในตัว .
นอกจากนี้เรายังเพิ่มการฉีดแบบพึ่งพา: ตัวสร้างการทดสอบใหม่ช่วยให้การทดสอบจัดหาค่าวันที่ปัจจุบันของตนเอง ตัวสร้างดั้งเดิมเรียกตัวสร้างใหม่นี้โดยส่งผ่านการอ้างอิงวิธีการแบบคงที่ของ LocalDate::now
ซึ่งให้ a LocalDate
วัตถุดังนั้นวิธีการหลักของเรายังคงใช้งานได้เหมือนเดิม แล้ววิธีการทดสอบของเราล่ะ? มาอัปเดตกัน PersonTest.java
:
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse('2013-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-17'); Person person = new Person('Joey', 'Doe', dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
ตอนนี้การทดสอบจะฉีดเอง currentDate
มูลค่าดังนั้นการทดสอบของเราจะยังคงผ่านเมื่อดำเนินการในปีหน้าหรือในช่วงปีใดก็ได้ โดยทั่วไปเรียกว่า กุด หรือระบุค่าที่ทราบว่าจะส่งคืน แต่ก่อนอื่นเราต้องเปลี่ยน Person
เพื่อให้สามารถฉีดการพึ่งพานี้ได้
หมายเหตุ ไวยากรณ์แลมบ์ดา (()->currentDate
) เมื่อสร้าง Person
วัตถุ. สิ่งนี้ถือเป็นซัพพลายเออร์ของ LocalDate
ตามที่ผู้สร้างใหม่กำหนด
เราพร้อมสำหรับ Person
ของเรา วัตถุที่มีอยู่ทั้งหมดอยู่ในหน่วยความจำ JVM เพื่อสื่อสารกับโลกภายนอก เราต้องการเพิ่มสองวิธี: publishAge()
ซึ่งจะโพสต์อายุปัจจุบันของบุคคลนั้นและ getThoseInCommon()
ซึ่งจะส่งคืนชื่อของบุคคลที่มีชื่อเสียงที่มีวันเกิดเดียวกันหรือมีอายุเท่ากับ Person
ของเรา สมมติว่ามีบริการ RESTful ที่เราสามารถโต้ตอบได้ชื่อว่า“ People Birthdays” เรามีไคลเอนต์ Java ที่ประกอบด้วยคลาสเดียว BirthdaysClient
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println('publishing ' + name + ''s age: ' + age); // HTTP POST with name and age and possibly throw an exception } public Collection findFamousNamesOfAge(long age) throws IOException { System.out.println('finding famous names of age ' + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println('finding famous names born on day ' + dayOfMonth + ' of month ' + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
มาปรับปรุง Person
ของเรา ชั้นเรียน เริ่มต้นด้วยการเพิ่มวิธีการทดสอบใหม่สำหรับพฤติกรรมที่ต้องการของ publishAge()
ทำไมต้องเริ่มต้นด้วยการทดสอบแทนที่จะเป็นฟังก์ชันการทำงาน? เราปฏิบัติตามหลักการของการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ (หรือที่เรียกว่า TDD) โดยเราจะเขียนการทดสอบก่อนแล้วจึงเขียนโค้ดเพื่อให้ผ่าน
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate); person.publishAge(); } }
ในตอนนี้โค้ดทดสอบไม่สามารถรวบรวมได้เนื่องจากเราไม่ได้สร้าง publishAge()
วิธีการโทร เมื่อเราสร้าง Person.publishAge()
ที่ว่างเปล่า วิธีการทุกอย่างผ่านไป ตอนนี้เราพร้อมสำหรับการทดสอบเพื่อยืนยันว่าอายุของบุคคลนั้นได้รับการเผยแพร่ไปยัง BirthdaysClient
แล้ว
เนื่องจากเป็นการทดสอบหน่วยจึงควรทำงานเร็วและอยู่ในหน่วยความจำดังนั้นการทดสอบจะสร้าง Person
ของเรา วัตถุจำลอง BirthdaysClient
ดังนั้นจึงไม่ได้ส่งคำขอทางเว็บจริงๆ จากนั้นการทดสอบจะใช้วัตถุจำลองนี้เพื่อตรวจสอบว่าถูกเรียกตามที่คาดไว้ ในการดำเนินการนี้เราจะเพิ่มการอ้างอิงในไฟล์ กรอบ Mockito (ใบอนุญาต MIT) สำหรับสร้างวัตถุจำลองแล้วสร้างจำลอง BirthdaysClient
วัตถุ:
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
นอกจากนี้เรายังเพิ่มลายเซ็นของ Person
ตัวสร้างที่จะใช้ BirthdaysClient
วัตถุและเปลี่ยนการทดสอบเป็นการฉีด BirthdaysClient
วัตถุ.
แนวทางปฏิบัติที่ดีที่สุดสำหรับสถาปัตยกรรมแอพ android
ต่อไปเราจะเพิ่มส่วนท้ายของ testPublishAge
ของเรา ความคาดหวังว่า BirthdaysClient
ถูกเรียก. Person.publishAge()
ควรเรียกมันตามที่แสดงใน PersonTest.java
ใหม่ของเรา:
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge('Joe Sixteen', 16); } }
Mockito ของเราปรับปรุง BirthdaysClient
ติดตามการโทรทั้งหมดที่ทำกับวิธีการซึ่งเป็นวิธีที่เราตรวจสอบว่าไม่มีการโทรไปที่ BirthdaysClient
ด้วย verifyZeroInteractions()
วิธีก่อนโทร publishAge()
. แม้ว่าเนื้อหาจะไม่จำเป็น แต่การทำเช่นนี้เรามั่นใจได้ว่าผู้สร้างไม่ได้ทำการโทรหลอกลวงใด ๆ บน verify()
เราระบุวิธีที่เราคาดหวังให้เรียก BirthdaysClient
มอง.
โปรดทราบว่าเนื่องจาก PublishRegularPersonAge มี IOException ในลายเซ็นเราจึงเพิ่มลงในลายเซ็นวิธีการทดสอบของเราด้วย
ณ จุดนี้การทดสอบล้มเหลว:
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( 'Joe Sixteen', 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
เป็นไปตามที่คาดไว้เนื่องจากเรายังไม่ได้ใช้การเปลี่ยนแปลงที่จำเป็นกับ Person.java
เนื่องจากเรากำลังติดตามการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ ตอนนี้เราจะทำการทดสอบนี้ผ่านโดยทำการเปลี่ยนแปลงที่จำเป็น:
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + ' ' + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
เราได้สร้างตัวสร้างรหัสการผลิตสร้างอินสแตนซ์ใหม่ BirthdaysClient
และ publishAge()
ตอนนี้เรียก birthdaysClient
การทดสอบทั้งหมดผ่าน; ทุกอย่างเป็นสีเขียว เยี่ยมมาก! แต่สังเกตว่า publishAge()
กำลังกลืน IOException แทนที่จะปล่อยให้ฟองออกเราต้องการห่อด้วย PersonException ของเราเองในไฟล์ใหม่ที่เรียกว่า PersonException.java
:
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
เราใช้สถานการณ์นี้เป็นวิธีการทดสอบใหม่ใน PersonTest.java
:
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge('Joe Sixteen', 16); try { person.publishAge(); fail('expected exception not thrown'); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals('Failed to publish Joe Sixteen age 16', e.getMessage()); } } }
Mockito doThrow()
ต้นขั้วการโทร birthdaysClient
เพื่อโยนข้อยกเว้นเมื่อ publishRegularPersonAge()
เรียกว่าวิธีการ ถ้า PersonException
ไม่ถูกโยนเราล้มเหลวในการทดสอบ มิฉะนั้นเรายืนยันว่าเป็นข้อยกเว้น ถูกล่ามโซ่อย่างถูกต้อง ด้วย IOException และตรวจสอบว่าข้อความข้อยกเว้นเป็นไปตามที่คาดไว้ ในตอนนี้เนื่องจากเรายังไม่ได้ใช้การจัดการใด ๆ ในรหัสการผลิตของเราการทดสอบของเราจึงล้มเหลวเนื่องจากไม่เกิดข้อยกเว้นที่คาดไว้ นี่คือสิ่งที่เราต้องเปลี่ยนแปลงใน Person.java
เพื่อให้ผ่านการทดสอบ:
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException('Failed to publish ' + nameToPublish + ' age ' + age, e); } } }
ตอนนี้เราใช้ Person.getThoseInCommon()
วิธีการทำให้ Person.Java
ของเรา ชั้นเรียนมีลักษณะ นี้ .
testGetThoseInCommon()
ของเราซึ่งแตกต่างจาก testPublishAge()
ไม่ยืนยันว่ามีการโทรไปยัง birthdaysClient
วิธีการ แทนที่จะใช้ when
เรียกเพื่อดึงค่าส่งคืนสำหรับการโทรไปยัง findFamousNamesOfAge()
และ findFamousNamesBornOn()
ที่ getThoseInCommon()
จะต้องทำ จากนั้นเรายืนยันว่าจะส่งคืนชื่อที่ขาดทั้งสามชื่อที่เราระบุ
การรวมคำยืนยันหลายรายการด้วย assertAll()
วิธีการ JUnit 5 อนุญาตให้ตรวจสอบการยืนยันทั้งหมดโดยรวมแทนที่จะหยุดหลังจากการยืนยันครั้งแรกที่ล้มเหลว นอกจากนี้เรายังรวมข้อความด้วย assertTrue()
เพื่อระบุชื่อเฉพาะที่ไม่รวมอยู่ด้วย วิธีการทดสอบ 'เส้นทางแห่งความสุข' (สถานการณ์ในอุดมคติ) มีลักษณะดังนี้ (โปรดทราบว่านี่ไม่ใช่ชุดการทดสอบที่มีประสิทธิภาพเนื่องจากเป็น 'เส้นทางแห่งความสุข' แต่เราจะพูดถึงสาเหตุในภายหลัง
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList('JoeFamous Sixteen', 'Another Person')); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList('Jan TwoKnown')); Set thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, 'Another Person'), setContains(thoseInCommon, 'Jan TwoKnown'), setContains(thoseInCommon, 'JoeFamous Sixteen'), ()-> assertEquals(3, thoseInCommon.size()) ); } private Executable setContains(Set set, T expected) { return () -> assertTrue(set.contains(expected), 'Should contain ' + expected); } // ... }
แม้ว่าจะถูกมองข้ามบ่อยครั้ง แต่ก็สำคัญไม่แพ้กันที่จะต้องรักษารหัสทดสอบให้ปราศจากความซ้ำซากจำเจ ทำความสะอาดรหัสและหลักการเช่น “ อย่าพูดซ้ำ” มีความสำคัญมากในการรักษาโค้ดเบสคุณภาพสูงการผลิตและโค้ดทดสอบเหมือนกัน โปรดสังเกตว่า PersonTest.java ล่าสุดมีการทำซ้ำบางส่วนในขณะนี้เรามีวิธีการทดสอบหลายวิธี
ในการแก้ไขปัญหานี้เราสามารถทำสิ่งต่างๆได้ดังนี้
แตกอ็อบเจ็กต์ IOException ลงในฟิลด์สุดท้ายส่วนตัว
แตก Person
การสร้างอ็อบเจกต์ในเมธอดของตัวเอง (createJoeSixteenJan2()
ในกรณีนี้) เนื่องจากอ็อบเจ็กต์ Person ส่วนใหญ่ถูกสร้างด้วยพารามิเตอร์เดียวกัน
สร้าง assertCauseAndMessage()
สำหรับการทดสอบต่างๆที่ตรวจสอบการโยน PersonExceptions
.
ผลลัพธ์คลีนโค้ดสามารถเห็นได้ในการแปลความหมายของ PersonTest.java ไฟล์.
เราควรทำอย่างไรเมื่อ a Person
วัตถุมีวันเดือนปีเกิดที่ช้ากว่าวันที่ปัจจุบัน? ข้อบกพร่องในแอปพลิเคชันมักเกิดจากการป้อนข้อมูลที่ไม่คาดคิดหรือขาดการมองการณ์ไกลในกรณีมุมขอบหรือขอบเขต สิ่งสำคัญคือต้องพยายามคาดการณ์สถานการณ์เหล่านี้ให้ดีที่สุดเท่าที่จะทำได้และการทดสอบหน่วยมักเป็นสถานที่ที่เหมาะสมในการทำเช่นนั้น ในการสร้าง Person
ของเรา และ PersonTest
เราได้รวมการทดสอบบางส่วนสำหรับข้อยกเว้นที่คาดไว้ แต่ก็ไม่สมบูรณ์ ตัวอย่างเช่นเราใช้ LocalDate
ซึ่งไม่ได้แสดงหรือจัดเก็บข้อมูลโซนเวลา การโทรของเราไปยัง LocalDate.now()
อย่างไรก็ตามส่งกลับ LocalDate
ตามเขตเวลาเริ่มต้นของระบบซึ่งอาจเร็วกว่าหรือช้ากว่าของผู้ใช้ระบบหนึ่งวัน ปัจจัยเหล่านี้ควรได้รับการพิจารณาด้วยการทดสอบและพฤติกรรมที่เหมาะสม
ควรทดสอบขอบเขตด้วย พิจารณา Person
วัตถุที่มี getDaysUntilBirthday()
วิธี. การทดสอบควรรวมถึงว่าวันเกิดของบุคคลนั้นผ่านไปแล้วหรือไม่ในปีปัจจุบันวันเกิดของบุคคลนั้นคือวันนี้หรือไม่และปีอธิกสุรทินมีผลต่อจำนวนวันอย่างไร สถานการณ์เหล่านี้ครอบคลุมได้โดยการตรวจสอบหนึ่งวันก่อนวันเกิดของบุคคลวันนั้นและหนึ่งวันหลังจากวันเกิดของบุคคลนั้นโดยที่ปีถัดไปเป็นปีอธิกสุรทิน นี่คือรหัสทดสอบที่เกี่ยวข้อง:
// ... class PersonTest { private final Supplier currentDateSupplier = ()-> LocalDate.parse('2015-05-02'); private final LocalDate ageJustOver5 = LocalDate.parse('2010-05-01'); private final LocalDate ageExactly5 = LocalDate.parse('2010-05-02'); private final LocalDate ageAlmost5 = LocalDate.parse('2010-05-03'); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function personLongFunction) { Person person = new Person('Given', 'Sur', dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
เราเน้นไปที่การทดสอบหน่วยเป็นส่วนใหญ่ แต่ JUnit ยังสามารถใช้สำหรับการทดสอบการรวมการยอมรับการทำงานและระบบ การทดสอบดังกล่าวมักต้องใช้รหัสการตั้งค่าเพิ่มเติมเช่นเซิร์ฟเวอร์เริ่มต้นการโหลดฐานข้อมูลด้วยข้อมูลที่ทราบเป็นต้นแม้ว่าเรามักจะเรียกใช้การทดสอบหลายพันหน่วยในไม่กี่วินาที แต่ชุดทดสอบการผสานรวมขนาดใหญ่อาจใช้เวลาหลายนาทีหรือหลายชั่วโมง โดยทั่วไปไม่ควรใช้การทดสอบการรวมเพื่อพยายามครอบคลุมทุกการเปลี่ยนแปลงหรือเส้นทางผ่านโค้ด การทดสอบหน่วยมีความเหมาะสมมากกว่าสำหรับสิ่งนั้น
การสร้างการทดสอบสำหรับเว็บแอปพลิเคชันที่ขับเคลื่อนเว็บเบราว์เซอร์ในการกรอกแบบฟอร์มการคลิกปุ่มรอให้เนื้อหาโหลด ฯลฯ มักทำได้โดยใช้ Selenium WebDriver (ใบอนุญาต Apache 2.0) ควบคู่ไปกับ 'Page Object Pattern' (ดูไฟล์ SeleniumHQ github wiki และ บทความของ Martin Fowler เกี่ยวกับ Page Objects ).
JUnit มีประสิทธิภาพในการทดสอบ RESTful API ด้วยการใช้ไคลเอนต์ HTTP เช่น Apache HTTP Client หรือ Spring Rest Template ( HowToDoInJava.com เป็นตัวอย่างที่ดี ).
ในกรณีของเรากับ Person
ออบเจ็กต์การทดสอบการรวมอาจเกี่ยวข้องกับการใช้จริง BirthdaysClient
แทนที่จะเป็นแบบจำลองโดยมีการกำหนดค่าระบุ URL ฐานของบริการ People Birthdays จากนั้นการทดสอบการผสานรวมจะใช้อินสแตนซ์การทดสอบของบริการดังกล่าวตรวจสอบว่ามีการเผยแพร่วันเกิดและสร้างบุคคลที่มีชื่อเสียงในบริการที่จะถูกส่งคืน
ความแตกต่างระหว่าง บริษัท c และ s
JUnit มีคุณสมบัติเพิ่มเติมมากมายที่เรายังไม่ได้สำรวจในตัวอย่าง เราจะอธิบายบางส่วนและให้ข้อมูลอ้างอิงสำหรับผู้อื่น
ควรสังเกตว่า JUnit สร้างอินสแตนซ์ใหม่ของคลาสทดสอบสำหรับการรันแต่ละ @Test
วิธี. JUnit ยังมี hooks คำอธิบายประกอบเพื่อเรียกใช้เมธอดเฉพาะก่อนหรือหลังทั้งหมดหรือแต่ละอัน @Test
วิธีการ ตะขอเหล่านี้มักใช้ในการตั้งค่าหรือล้างฐานข้อมูลหรือวัตถุจำลองและแตกต่างกันระหว่าง JUnit 4 และ 5
JUnit 4 | JUnit 5 | สำหรับวิธีคงที่? |
---|---|---|
@BeforeClass | @BeforeAll | ใช่ |
@AfterClass | @AfterAll | ใช่ |
@Before | @BeforeEach | ไม่ |
@After | @AfterEach | ไม่ |
ใน PersonTest
ของเรา ตัวอย่างเช่นเราเลือกกำหนดค่า BirthdaysClient
วัตถุจำลองใน @Test
วิธีการเอง แต่บางครั้งโครงสร้างจำลองที่ซับซ้อนกว่านั้นจำเป็นต้องสร้างขึ้นโดยเกี่ยวข้องกับวัตถุหลายชิ้น @BeforeEach
(ใน JUnit 5) และ @Before
(ใน JUnit 4) มักจะเหมาะสมสำหรับสิ่งนี้
@After*
คำอธิบายประกอบมักใช้กับการทดสอบการรวมมากกว่าการทดสอบหน่วยเนื่องจากการรวบรวมขยะ JVM จัดการกับวัตถุส่วนใหญ่ที่สร้างขึ้นสำหรับการทดสอบหน่วย @BeforeClass
และ @BeforeAll
คำอธิบายประกอบมักใช้สำหรับการทดสอบการรวมที่จำเป็นต้องดำเนินการตั้งค่าและฉีกขาดเพียงครั้งเดียวแทนที่จะใช้สำหรับวิธีทดสอบแต่ละวิธี
สำหรับ JUnit 4 โปรดดูที่ไฟล์ คู่มือการติดตั้งการทดสอบ (แนวคิดทั่วไปยังคงใช้กับ JUnit 5)
บางครั้งคุณต้องการเรียกใช้การทดสอบที่เกี่ยวข้องหลายรายการ แต่ไม่ใช่การทดสอบทั้งหมด ในกรณีนี้สามารถจัดกลุ่มการทดสอบเป็นชุดทดสอบได้ สำหรับวิธีการทำใน JUnit 5 โปรดดู บทความ JUnit 5 ของ HowToProgram.xyz และในทีม JUnit เอกสารประกอบสำหรับ JUnit 4 .
JUnit 5 เพิ่มความสามารถในการใช้คลาสภายในที่ซ้อนกันแบบไม่คงที่เพื่อแสดงความสัมพันธ์ระหว่างการทดสอบได้ดีขึ้น สิ่งนี้น่าจะคุ้นเคยเป็นอย่างดีสำหรับผู้ที่เคยทำงานกับคำอธิบายที่ซ้อนกันในกรอบการทดสอบเช่น Jasmine สำหรับ JavaScript ชั้นเรียนด้านในมีคำอธิบายประกอบ @Nested
เพื่อใช้สิ่งนี้
@DisplayName
นอกจากนี้คำอธิบายประกอบยังใหม่สำหรับ JUnit 5 ซึ่งช่วยให้คุณสามารถอธิบายการทดสอบสำหรับการรายงานในรูปแบบสตริงที่จะแสดงเพิ่มเติมจากตัวระบุวิธีการทดสอบ
แม้ว่า @Nested
และ @DisplayName
สามารถใช้งานได้โดยอิสระจากกันซึ่งสามารถให้ผลการทดสอบที่ชัดเจนขึ้นซึ่งอธิบายถึงพฤติกรรมของระบบ
กรอบ Hamcrest แม้ว่าจะไม่ได้เป็นส่วนหนึ่งของ JUnit codebase แต่ก็เป็นอีกทางเลือกหนึ่งนอกเหนือจากการใช้วิธีการยืนยันแบบเดิมในการทดสอบซึ่งช่วยให้สามารถอ่านรหัสทดสอบที่แสดงออกและอ่านได้ง่าย ดูการตรวจสอบดังต่อไปนี้โดยใช้ทั้ง assertEquals แบบเดิมและ Hamcrest assertThat:
//Traditional assert assertEquals('Hayden, Josh', displayName); //Hamcrest assert assertThat(displayName, equalTo('Hayden, Josh'));
Hamcrest สามารถใช้ได้กับทั้ง JUnit 4 และ 5 บทช่วยสอนของ Vogella.com เกี่ยวกับ Hamcrest ค่อนข้างครอบคลุม
บทความ การทดสอบหน่วยวิธีการเขียนโค้ดที่ทดสอบได้และเหตุใดจึงสำคัญ ครอบคลุมตัวอย่างเฉพาะเพิ่มเติมของการเขียนโค้ดที่สะอาดและทดสอบได้
สร้างด้วยความมั่นใจ: คำแนะนำในการทดสอบ JUnit ตรวจสอบวิธีการต่างๆในการทดสอบหน่วยและการผสานรวมและเหตุใดจึงควรเลือกวิธีใดวิธีหนึ่งและยึดติดกับมัน
JUnit 4 Wiki และ คู่มือผู้ใช้ JUnit 5 เป็นจุดอ้างอิงที่ยอดเยี่ยมเสมอ
เอกสาร Mockito ให้ข้อมูลเกี่ยวกับฟังก์ชันและตัวอย่างเพิ่มเติม
เราได้สำรวจหลายแง่มุมของการทดสอบในโลก Java ด้วย JUnit เราได้ดูการทดสอบหน่วยและการรวมโดยใช้เฟรมเวิร์ก JUnit สำหรับโค้ดฐาน Java การรวม JUnit ในการพัฒนาและสร้างสภาพแวดล้อมวิธีใช้ mocks และ Stubs กับซัพพลายเออร์และ Mockito อนุสัญญาทั่วไปและแนวทางปฏิบัติด้านโค้ดที่ดีที่สุดสิ่งที่ต้องทดสอบและบางส่วนของ คุณสมบัติอื่น ๆ ของ JUnit
ตอนนี้ผู้อ่านหันมาเติบโตในการประยุกต์ใช้ดูแลรักษาและเก็บเกี่ยวประโยชน์จากการทดสอบอัตโนมัติโดยใช้กรอบ JUnit อย่างชำนาญ