portaldacalheta.pt
  • หลัก
  • การจัดการวิศวกรรม
  • บุคลากรและทีมงานของผลิตภัณฑ์
  • อื่น ๆ
  • นวัตกรรม
แบ็คเอนด์

คู่มือสำหรับการทดสอบหน่วยที่แข็งแกร่งและการรวมเข้ากับ JUnit



การทดสอบซอฟต์แวร์อัตโนมัติมีความสำคัญอย่างยิ่งต่อคุณภาพในระยะยาวความสามารถในการบำรุงรักษาและความสามารถในการขยายโครงการซอฟต์แวร์และสำหรับ Java JUnit เป็นเส้นทางสู่ระบบอัตโนมัติ

ในขณะที่บทความนี้ส่วนใหญ่จะเน้นไปที่การเขียนการทดสอบหน่วยที่มีประสิทธิภาพและการใช้การทดสอบการลอกเลียนแบบและการพึ่งพา แต่เราจะพูดถึงการทดสอบ JUnit และการรวม



กรอบการทดสอบ JUnit เป็นเครื่องมือทั่วไปฟรีและโอเพ่นซอร์สสำหรับการทดสอบโครงการที่ใช้ Java



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



JUnit 5 (ด้วยโมเดลการเขียนโปรแกรม Jupiter และส่วนขยาย) อยู่ในระหว่างการพัฒนา รองรับคุณสมบัติภาษาที่แนะนำใน จาวา 8 และรวมถึงคุณสมบัติใหม่ที่น่าสนใจอื่น ๆ บางทีมอาจพบว่า JUnit 5 พร้อมใช้งานในขณะที่บางทีมอาจใช้ JUnit 4 ต่อไปจนกว่า 5 จะวางจำหน่ายอย่างเป็นทางการ เราจะดูตัวอย่างจากทั้งสองอย่าง

กำลังรัน JUnit

การทดสอบ JUnit สามารถรันได้โดยตรงใน IntelliJ แต่ยังสามารถรันใน IDE อื่น ๆ เช่น Eclipse, NetBeans หรือแม้แต่บรรทัดคำสั่ง



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

การทดสอบ JUnit ยังสามารถรันและรายงานโดยระบบการผสานรวมแบบต่อเนื่องเช่น Jenkins โครงการที่ใช้เครื่องมือเช่น Gradle, Maven หรือ Ant มีข้อได้เปรียบเพิ่มเติมคือสามารถเรียกใช้การทดสอบโดยเป็นส่วนหนึ่งของกระบวนการสร้าง



กลุ่มเกียร์ที่ระบุความเข้ากันได้: JUnit 4 กับ NetBeans ในหนึ่งเดียว JUnit 5 พร้อม Eclipse และ Gradle ในอีกอันและสุดท้ายที่มี JUnit 5 กับ Maven และ IntelliJ IDEA

Gradle

เป็นตัวอย่างโครงการ 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 .



Maven

สำหรับ 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

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

NetBeans รองรับการทดสอบ JUnit 4 เท่านั้น สามารถสร้างคลาสทดสอบในโปรเจ็กต์ NetBeans Java ด้วย File> New File …> Unit Tests> JUnit Test หรือ Test for Existing Class ตามค่าเริ่มต้นไดเร็กทอรีรูททดสอบจะมีชื่อว่า test ในไดเรกทอรีโครงการ

คลาสการผลิตอย่างง่ายและกรณีทดสอบ JUnit

มาดูตัวอย่างโค้ดการผลิตและโค้ดทดสอบหน่วยที่เกี่ยวข้องกันง่ายๆสำหรับ Person ชั้นเรียน คุณสามารถดาวน์โหลดโค้ดตัวอย่างจากไฟล์ โครงการ github และเปิดผ่าน IntelliJ

src / main / java / com / example / Person.java
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):

src / test / java / com / example / PersonTest.java
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 เป็นสีเขียว

อนุสัญญา JUnit ทั่วไป

การตั้งชื่อ

แม้ว่าจะไม่จำเป็น แต่เราก็ใช้หลักการทั่วไปในการตั้งชื่อคลาสทดสอบ โดยเฉพาะเราเริ่มต้นด้วยชื่อของชั้นเรียนที่กำลังทดสอบ (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 ชั้นเรียนดูเหมือนว่า:

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 ปี เพิ่มวิธีการทดสอบ:

PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); long age = person.getAge(); assertEquals(4, age); } }

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

Stubbing and Injecting a Value Supplier

เมื่อดำเนินการผลิตเราต้องการใช้วันที่ปัจจุบัน LocalDate.now() ในการคำนวณอายุของบุคคล แต่ในการทำการทดสอบเชิงกำหนดแม้ในหนึ่งปีนับจากนี้การทดสอบจำเป็นต้องจัดหา currentDate ของตนเอง ค่า

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

มาเพิ่ม LocalDate ผู้จัดหาให้ Person.java:

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:

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

com.example.birthdays.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) โดยเราจะเขียนการทดสอบก่อนแล้วจึงเขียนโค้ดเพื่อให้ผ่าน

PersonTest.java
// … 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 วัตถุ:

PersonTest.java
// ... 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 ใหม่ของเรา:

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 เนื่องจากเรากำลังติดตามการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ ตอนนี้เราจะทำการทดสอบนี้ผ่านโดยทำการเปลี่ยนแปลงที่จำเป็น:

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:

PersonException.java
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }

เราใช้สถานการณ์นี้เป็นวิธีการทดสอบใหม่ใน PersonTest.java:

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 เพื่อให้ผ่านการทดสอบ:

Person.java
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException('Failed to publish ' + nameToPublish + ' age ' + age, e); } } }

Stubs: เมื่อไรและการยืนยัน

ตอนนี้เราใช้ Person.getThoseInCommon() วิธีการทำให้ Person.Java ของเรา ชั้นเรียนมีลักษณะ นี้ .

testGetThoseInCommon() ของเราซึ่งแตกต่างจาก testPublishAge() ไม่ยืนยันว่ามีการโทรไปยัง birthdaysClient วิธีการ แทนที่จะใช้ when เรียกเพื่อดึงค่าส่งคืนสำหรับการโทรไปยัง findFamousNamesOfAge() และ findFamousNamesBornOn() ที่ getThoseInCommon() จะต้องทำ จากนั้นเรายืนยันว่าจะส่งคืนชื่อที่ขาดทั้งสามชื่อที่เราระบุ

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

PersonTest.java
// ... 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() วิธี. การทดสอบควรรวมถึงว่าวันเกิดของบุคคลนั้นผ่านไปแล้วหรือไม่ในปีปัจจุบันวันเกิดของบุคคลนั้นคือวันนี้หรือไม่และปีอธิกสุรทินมีผลต่อจำนวนวันอย่างไร สถานการณ์เหล่านี้ครอบคลุมได้โดยการตรวจสอบหนึ่งวันก่อนวันเกิดของบุคคลวันนั้นและหนึ่งวันหลังจากวันเกิดของบุคคลนั้นโดยที่ปีถัดไปเป็นปีอธิกสุรทิน นี่คือรหัสทดสอบที่เกี่ยวข้อง:

PersonTest.java
// ... 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 มีคุณสมบัติเพิ่มเติมมากมายที่เรายังไม่ได้สำรวจในตัวอย่าง เราจะอธิบายบางส่วนและให้ข้อมูลอ้างอิงสำหรับผู้อื่น

ทดสอบการแข่งขัน

ควรสังเกตว่า 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’s @Nested และ @DisplayName

JUnit 5 เพิ่มความสามารถในการใช้คลาสภายในที่ซ้อนกันแบบไม่คงที่เพื่อแสดงความสัมพันธ์ระหว่างการทดสอบได้ดีขึ้น สิ่งนี้น่าจะคุ้นเคยเป็นอย่างดีสำหรับผู้ที่เคยทำงานกับคำอธิบายที่ซ้อนกันในกรอบการทดสอบเช่น Jasmine สำหรับ JavaScript ชั้นเรียนด้านในมีคำอธิบายประกอบ @Nested เพื่อใช้สิ่งนี้

@DisplayName นอกจากนี้คำอธิบายประกอบยังใหม่สำหรับ JUnit 5 ซึ่งช่วยให้คุณสามารถอธิบายการทดสอบสำหรับการรายงานในรูปแบบสตริงที่จะแสดงเพิ่มเติมจากตัวระบุวิธีการทดสอบ

แม้ว่า @Nested และ @DisplayName สามารถใช้งานได้โดยอิสระจากกันซึ่งสามารถให้ผลการทดสอบที่ชัดเจนขึ้นซึ่งอธิบายถึงพฤติกรรมของระบบ

Hamcrest Matchers

กรอบ 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 ให้ข้อมูลเกี่ยวกับฟังก์ชันและตัวอย่างเพิ่มเติม

JUnit คือเส้นทางสู่ระบบอัตโนมัติ

เราได้สำรวจหลายแง่มุมของการทดสอบในโลก Java ด้วย JUnit เราได้ดูการทดสอบหน่วยและการรวมโดยใช้เฟรมเวิร์ก JUnit สำหรับโค้ดฐาน Java การรวม JUnit ในการพัฒนาและสร้างสภาพแวดล้อมวิธีใช้ mocks และ Stubs กับซัพพลายเออร์และ Mockito อนุสัญญาทั่วไปและแนวทางปฏิบัติด้านโค้ดที่ดีที่สุดสิ่งที่ต้องทดสอบและบางส่วนของ คุณสมบัติอื่น ๆ ของ JUnit

ตอนนี้ผู้อ่านหันมาเติบโตในการประยุกต์ใช้ดูแลรักษาและเก็บเกี่ยวประโยชน์จากการทดสอบอัตโนมัติโดยใช้กรอบ JUnit อย่างชำนาญ

รายงาน: สถานะของพนักงาน

อนาคตของการทำงาน

รายงาน: สถานะของพนักงาน
ApeeScape Finance ขับเคลื่อนอนาคตของการทำงานเพื่อส่งมอบความเชี่ยวชาญด้านการเงินตามความต้องการ

ApeeScape Finance ขับเคลื่อนอนาคตของการทำงานเพื่อส่งมอบความเชี่ยวชาญด้านการเงินตามความต้องการ

อื่น ๆ

โพสต์ยอดนิยม
แหล่งข้อมูลสำหรับธุรกิจขนาดเล็กสำหรับ COVID-19: เงินกู้เงินช่วยเหลือและสินเชื่อ
แหล่งข้อมูลสำหรับธุรกิจขนาดเล็กสำหรับ COVID-19: เงินกู้เงินช่วยเหลือและสินเชื่อ
วิธีออกแบบประสบการณ์ที่ยอดเยี่ยมสำหรับอินเทอร์เน็ตในทุกสิ่ง
วิธีออกแบบประสบการณ์ที่ยอดเยี่ยมสำหรับอินเทอร์เน็ตในทุกสิ่ง
กลยุทธ์การสื่อสารที่มีประสิทธิภาพสำหรับนักออกแบบ
กลยุทธ์การสื่อสารที่มีประสิทธิภาพสำหรับนักออกแบบ
เรียนรู้ Markdown: เครื่องมือการเขียนสำหรับนักพัฒนาซอฟต์แวร์
เรียนรู้ Markdown: เครื่องมือการเขียนสำหรับนักพัฒนาซอฟต์แวร์
แนวโน้มต่อไปนี้: การแสดงความเคารพกับการลอกเลียนแบบการออกแบบ
แนวโน้มต่อไปนี้: การแสดงความเคารพกับการลอกเลียนแบบการออกแบบ
 
คู่มือสไตล์ Sass: บทช่วยสอน Sass เกี่ยวกับวิธีการเขียนโค้ด CSS ที่ดีขึ้น
คู่มือสไตล์ Sass: บทช่วยสอน Sass เกี่ยวกับวิธีการเขียนโค้ด CSS ที่ดีขึ้น
ทำลายกระบวนการคิดเชิงออกแบบ
ทำลายกระบวนการคิดเชิงออกแบบ
การออกแบบเว็บไซต์ CMS: คู่มือการใช้งานเนื้อหาแบบไดนามิก
การออกแบบเว็บไซต์ CMS: คู่มือการใช้งานเนื้อหาแบบไดนามิก
ทำคณิตศาสตร์: การปรับขนาดแอปพลิเคชันไมโครเซอร์วิสด้วย Orchestrators
ทำคณิตศาสตร์: การปรับขนาดแอปพลิเคชันไมโครเซอร์วิสด้วย Orchestrators
การปฏิวัติหุ่นยนต์เชิงพาณิชย์ที่กำลังจะเกิดขึ้น
การปฏิวัติหุ่นยนต์เชิงพาณิชย์ที่กำลังจะเกิดขึ้น
โพสต์ยอดนิยม
  • คอมไพล์ไฟล์ c++
  • วิธีทำงบกระแสเงินสดจากงบดุล
  • วิธีทำกูเกิลแก้ว
  • การซื้อกิจการที่มีเลเวอเรจ (lbo)
  • c คอร์ปอเรชั่น vs s corp
หมวดหมู่
  • การจัดการวิศวกรรม
  • บุคลากรและทีมงานของผลิตภัณฑ์
  • อื่น ๆ
  • นวัตกรรม
  • © 2022 | สงวนลิขสิทธิ์

    portaldacalheta.pt