วันนี้คุณจะไม่พบแอปพลิเคชันที่ไม่มีการทดสอบ ดังนั้นหัวข้อนี้จะมีความเกี่ยวข้องมากกว่าที่เคยสำหรับนักพัฒนามือใหม่: คุณไม่สามารถประสบความสำเร็จได้หากไม่มีการทดสอบ ลองพิจารณาว่าการทดสอบประเภทใดที่ใช้ในหลักการ จากนั้นเราจะศึกษารายละเอียดทุกอย่างที่ควรรู้เกี่ยวกับการทดสอบหน่วย
ประเภทของการทดสอบ
การทดสอบคืออะไร? ตาม Wikipedia: "การทดสอบซอฟต์แวร์เกี่ยวข้องกับการดำเนินการของส่วนประกอบซอฟต์แวร์หรือส่วนประกอบของระบบเพื่อประเมินคุณสมบัติที่น่าสนใจอย่างน้อยหนึ่งรายการ" กล่าวอีกนัยหนึ่งคือการตรวจสอบความถูกต้องของระบบของเราในบางสถานการณ์ เรามาดูกันว่าโดยทั่วไปมีการทดสอบประเภทใดบ้าง:- การทดสอบหน่วย — การทดสอบที่มีจุดประสงค์เพื่อตรวจสอบแต่ละโมดูลของระบบแยกกัน การทดสอบเหล่านี้ควรใช้กับชิ้นส่วนอะตอมที่เล็กที่สุดของระบบ เช่น โมดูล
- การทดสอบระบบ — การทดสอบระดับสูงเพื่อตรวจสอบการทำงานของแอปพลิเคชันชิ้นใหญ่หรือระบบโดยรวม
- การทดสอบการถดถอย — การทดสอบที่ใช้เพื่อตรวจสอบว่าคุณสมบัติใหม่หรือการแก้ไขจุดบกพร่องส่งผลกระทบต่อการทำงานที่มีอยู่ของแอปพลิเคชันหรือทำให้เกิดจุดบกพร่องเก่าหรือไม่
- การทดสอบการทำงาน — การตรวจสอบว่าส่วนหนึ่งของแอปพลิเคชันเป็นไปตามข้อกำหนดที่ระบุไว้ในข้อมูลจำเพาะ เรื่องราวของผู้ใช้ ฯลฯ หรือไม่
ประเภทของการทดสอบการทำงาน:
- การทดสอบกล่องขาว — การตรวจสอบว่าส่วนหนึ่งของแอปพลิเคชันเป็นไปตามข้อกำหนดหรือไม่ ในขณะที่ทราบการใช้งานภายในระบบ
- การทดสอบกล่องดำ — การตรวจสอบว่าส่วนหนึ่งของแอปพลิเคชันเป็นไปตามข้อกำหนดหรือไม่ โดยไม่ทราบการใช้งานภายในระบบ
- การทดสอบประสิทธิภาพ — การทดสอบที่เขียนขึ้นเพื่อกำหนดวิธีการทำงานของระบบหรือส่วนหนึ่งของระบบภายใต้โหลดที่กำหนด
- การทดสอบโหลด — การทดสอบที่ออกแบบมาเพื่อตรวจสอบความเสถียรของระบบภายใต้โหลดมาตรฐาน และเพื่อหาโหลดสูงสุดที่แอปพลิเคชันยังทำงานได้อย่างถูกต้อง
- การทดสอบความเค้น — การทดสอบที่ออกแบบมาเพื่อตรวจสอบประสิทธิภาพของแอปพลิเคชันภายใต้โหลดที่ไม่ได้มาตรฐาน และเพื่อกำหนดโหลดสูงสุดก่อนที่ระบบจะล้มเหลว
- การทดสอบความปลอดภัย — การทดสอบที่ใช้เพื่อตรวจสอบความปลอดภัยของระบบ (จากแฮกเกอร์ ไวรัส การเข้าถึงข้อมูลที่เป็นความลับโดยไม่ได้รับอนุญาต และการโจมตีที่น่ายินดีอื่นๆ)
- การทดสอบการแปลเป็นภาษาท้องถิ่น — การทดสอบการแปลแอปพลิเคชันเป็นภาษาท้องถิ่น
- การทดสอบความสามารถในการใช้งาน — การทดสอบที่มุ่งตรวจสอบความสามารถในการใช้งาน ความสามารถในการเข้าใจ ความน่าดึงดูดใจ และความสามารถในการเรียนรู้
- หน่วย — ส่วนนี้หมายถึงการทดสอบหน่วย ซึ่งนำไปใช้ในชั้นต่างๆ ของแอปพลิเคชัน พวกเขาทดสอบหน่วยที่เล็กที่สุดของตรรกะของแอปพลิเคชันที่หารลงตัว ตัวอย่างเช่นคลาส แต่ส่วนใหญ่มักจะเป็นวิธีการ การทดสอบเหล่านี้มักจะพยายามแยกสิ่งที่ทดสอบออกจากตรรกะภายนอกให้ได้มากที่สุด นั่นคือพวกเขาพยายามสร้างภาพลวงตาว่าแอปพลิเคชันที่เหลือกำลังทำงานตามที่คาดไว้
ควรมีการทดสอบประเภทนี้จำนวนมากเสมอ (มากกว่าประเภทอื่น ๆ ) เนื่องจากการทดสอบเหล่านี้มีขนาดเล็กและน้ำหนักเบามาก ไม่ใช้ทรัพยากรมาก (หมายถึง RAM และเวลา)
- การรวมระบบ — ส่วนนี้อ้างถึงการทดสอบการรวมระบบ การทดสอบนี้ตรวจสอบชิ้นส่วนขนาดใหญ่ของระบบ นั่นคือรวมตรรกะหลายส่วน (หลายเมธอดหรือหลายคลาส) หรือตรวจสอบความถูกต้องของการโต้ตอบกับส่วนประกอบภายนอก การทดสอบเหล่านี้มักจะมีขนาดเล็กกว่าการทดสอบหน่วยเนื่องจากหนักกว่า
ตัวอย่างของการทดสอบการรวมอาจเป็นการเชื่อมต่อกับฐานข้อมูลและตรวจสอบความถูกต้องของการทำงานของวิธีการทำงานกับมัน
- UI — ส่วนนี้หมายถึงการทดสอบที่ตรวจสอบการทำงานของอินเทอร์เฟซผู้ใช้ ซึ่งเกี่ยวข้องกับตรรกะในทุกระดับของแอปพลิเคชัน ซึ่งเป็นสาเหตุที่เรียกอีกอย่างว่าการทดสอบแบบ end-to-end ตามกฎแล้วมีน้อยกว่ามากเนื่องจากเป็นเส้นทางที่ยุ่งยากที่สุดและต้องตรวจสอบเส้นทาง (ที่ใช้) ที่จำเป็นที่สุด
ในภาพด้านบน เราเห็นว่าส่วนต่างๆ ของสามเหลี่ยมมีขนาดแตกต่างกันไป: สัดส่วนที่เท่ากันโดยประมาณมีอยู่ในจำนวนการทดสอบประเภทต่างๆ ในการทำงานจริง
วันนี้เราจะมาดูการทดสอบที่พบบ่อยที่สุดอย่างละเอียดยิ่งขึ้น การทดสอบหน่วย เนื่องจากนักพัฒนา Java ที่เคารพตนเองทุกคนควรใช้งานได้ในระดับพื้นฐาน
แนวคิดหลักในการทดสอบหน่วย
ความครอบคลุมการทดสอบ (ความครอบคลุมของรหัส) เป็นหนึ่งในมาตรการหลักสำหรับการทดสอบแอปพลิเคชัน นี่คือเปอร์เซ็นต์ของรหัสที่ครอบคลุมโดยการทดสอบ (0-100%) ในทางปฏิบัติ หลายคนใช้เปอร์เซ็นต์นี้เป็นเป้าหมาย นั่นเป็นสิ่งที่ฉันไม่เห็นด้วย เพราะมันหมายความว่าการทดสอบเริ่มนำไปใช้ในที่ที่ไม่จำเป็น ตัวอย่างเช่น สมมติว่าเรามีการดำเนินการ CRUD (สร้าง/รับ/อัปเดต/ลบ) มาตรฐานในบริการของเราโดยไม่มีตรรกะเพิ่มเติม เมธอดเหล่านี้เป็นเพียงตัวกลางที่มอบงานให้กับเลเยอร์ที่ทำงานกับที่เก็บ ในสถานการณ์นี้ เราไม่มีอะไรต้องทดสอบ ยกเว้นว่าเมธอดที่กำหนดจะเรียกเมธอด DAO หรือไม่ แต่นั่นก็เป็นเรื่องตลก เครื่องมือเพิ่มเติมมักจะใช้เพื่อประเมินความครอบคลุมของการทดสอบ: JaCoCo, Cobertura, Clover, Emma เป็นต้น หากต้องการศึกษารายละเอียดเพิ่มเติมเกี่ยวกับหัวข้อนี้ TDD หมายถึงการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ ในแนวทางนี้ ก่อนที่จะทำสิ่งอื่นใด คุณต้องเขียนการทดสอบที่จะตรวจสอบรหัสเฉพาะ สิ่งนี้กลายเป็นการทดสอบแบบกล่องดำ เรารู้ว่าอินพุตคืออะไร และเรารู้ว่าเอาต์พุตควรเป็นอย่างไร สิ่งนี้ทำให้สามารถหลีกเลี่ยงการทำซ้ำรหัสได้ การพัฒนาโดยใช้การทดสอบเริ่มต้นด้วยการออกแบบและพัฒนาการทดสอบสำหรับฟังก์ชันการทำงานแต่ละบิตในแอปพลิเคชันของคุณ ในแนวทาง TDD อันดับแรก เราจะสร้างการทดสอบที่กำหนดและทดสอบพฤติกรรมของโค้ด เป้าหมายหลักของ TDD คือการทำให้โค้ดของคุณเข้าใจได้ง่ายขึ้น ง่ายขึ้น และปราศจากข้อผิดพลาด วิธีการประกอบด้วยดังต่อไปนี้:- เราเขียนการทดสอบของเรา
- เราทำการทดสอบ ไม่น่าแปลกใจที่มันล้มเหลว เนื่องจากเรายังไม่ได้ใช้ตรรกะที่จำเป็น
- เพิ่มรหัสที่ทำให้การทดสอบผ่าน (เราทำการทดสอบอีกครั้ง)
- เราปรับโครงสร้างรหัสใหม่
ขั้นตอนของการทดสอบ
การทดสอบประกอบด้วยสามขั้นตอน:- ระบุข้อมูลการทดสอบ (การแข่งขัน)
- ใช้รหัสภายใต้การทดสอบ (เรียกวิธีการทดสอบ)
- ตรวจสอบผลลัพธ์และเปรียบเทียบกับผลลัพธ์ที่คาดหวัง
สภาพแวดล้อมการทดสอบ
ดังนั้นตอนนี้ถึงจุด มีสภาพแวดล้อมการทดสอบ (เฟรมเวิร์ก) มากมายสำหรับ Java ที่นิยมมากที่สุดคือ JUnit และ TestNG สำหรับการตรวจสอบของเราที่นี่ เราใช้: การทดสอบ JUnit เป็นวิธีการในชั้นเรียนที่ใช้สำหรับการทดสอบเท่านั้น ชั้นเรียนมักจะตั้งชื่อเหมือนกับชั้นเรียนที่ทดสอบ โดยมี "การทดสอบ" ต่อท้าย ตัวอย่างเช่น CarService -> CarServiceTest ระบบการสร้าง Maven จะรวมคลาสดังกล่าวไว้ในขอบเขตการทดสอบโดยอัตโนมัติ อันที่จริงคลาสนี้เรียกว่าคลาสทดสอบ มาดูคำอธิบายประกอบพื้นฐานโดยสังเขป:- @Test บ่งชี้ว่าเมธอดนั้นเป็นการทดสอบ (โดยทั่วไป เมธอดที่มีคำอธิบายประกอบนี้คือการทดสอบหน่วย)
- @Before หมายถึงวิธีการที่จะดำเนินการก่อนการทดสอบแต่ละครั้ง ตัวอย่างเช่น เพื่อเติมข้อมูลในคลาสด้วยข้อมูลการทดสอบ อ่านข้อมูลอินพุต ฯลฯ
- @After ใช้เพื่อทำเครื่องหมายเมธอดที่จะเรียกใช้หลังจากการทดสอบแต่ละครั้ง (เช่น เพื่อล้างข้อมูลหรือกู้คืนค่าเริ่มต้น)
- @BeforeClass วางอยู่เหนือเมธอด คล้ายกับ @Before แต่เมธอดดังกล่าวถูกเรียกใช้เพียงครั้งเดียวก่อนการทดสอบทั้งหมดสำหรับคลาสที่กำหนด ดังนั้นจึงต้องเป็นแบบคงที่ ใช้เพื่อดำเนินการที่ใช้ทรัพยากรมากขึ้น เช่น การปั่นฐานข้อมูลทดสอบ
- @AfterClass ตรงกันข้ามกับ @BeforeClass: มันถูกเรียกใช้งานครั้งเดียวสำหรับคลาสที่กำหนด แต่หลังจากการทดสอบทั้งหมดเท่านั้น ตัวอย่างเช่น ใช้เพื่อล้างทรัพยากรถาวรหรือยกเลิกการเชื่อมต่อจากฐานข้อมูล
- @Ignore แสดงว่าเมธอดถูกปิดใช้งานและจะถูกละเว้นในระหว่างการทดสอบโดยรวม ซึ่งใช้ในสถานการณ์ต่างๆ เช่น ถ้าเมธอดฐานมีการเปลี่ยนแปลงและการทดสอบยังไม่ได้ทำใหม่เพื่อรองรับการเปลี่ยนแปลง ในกรณีเช่นนี้ ขอแนะนำให้เพิ่มคำอธิบาย เช่น @Ignore("Some description")
- @Test(expected = Exception.class) ใช้สำหรับการทดสอบเชิงลบ นี่คือการทดสอบที่ตรวจสอบว่าเมธอดทำงานอย่างไรในกรณีที่เกิดข้อผิดพลาด นั่นคือ การทดสอบคาดว่าเมธอดจะโยนข้อยกเว้นบางประเภทออกไป วิธีการดังกล่าวระบุโดยคำอธิบายประกอบ @Test แต่มีการระบุข้อผิดพลาดที่จะตรวจจับ
- @Test(timeout = 100) ตรวจสอบว่าวิธีการดำเนินการในเวลาไม่เกิน 100 มิลลิวินาที
- @Mock ใช้เหนือฟิลด์เพื่อกำหนดวัตถุจำลอง (นี่ไม่ใช่คำอธิบายประกอบ JUnit แต่มาจาก Mockito แทน) หากจำเป็น เราจะตั้งค่าพฤติกรรมจำลองสำหรับสถานการณ์เฉพาะโดยตรงในวิธีการทดสอบ
- @RunWith(MockitoJUnitRunner.class) ถูกวางไว้เหนือคลาส คำอธิบายประกอบนี้บอกให้ JUnit เรียกใช้การทดสอบในชั้นเรียน มีนักวิ่งหลายประเภท ได้แก่ MockitoJUnitRunner, JUnitPlatform และ SpringRunner ใน JUnit 5 คำอธิบายประกอบ @RunWith ได้ถูกแทนที่ด้วยคำอธิบายประกอบ @ExtendWith ที่มีประสิทธิภาพมากขึ้น
- assertEquals(วัตถุที่คาดไว้, วัตถุจริง) — ตรวจสอบว่าวัตถุที่ส่งผ่านมีค่าเท่ากันหรือไม่
- assertTrue(boolean flag) — ตรวจสอบว่าค่าที่ส่งผ่านเป็นจริงหรือไม่
- assertFalse(boolean flag) — ตรวจสอบว่าค่าที่ส่งผ่านเป็นเท็จหรือไม่
- assertNull(Object object) — ตรวจสอบว่า object ที่ผ่านเป็น null หรือไม่
- assertSame(Object firstObject, Object secondObject) — ตรวจสอบว่าค่าที่ส่งผ่านอ้างถึงวัตถุเดียวกันหรือไม่
- ยืนยันว่า(T t, Matcher
จับคู่) — ตรวจสอบว่า t เป็นไปตามเงื่อนไขที่ระบุในการจับคู่หรือไม่
การทดสอบในทางปฏิบัติ
ตอนนี้ มาดูเนื้อหาข้างต้นในตัวอย่างที่เฉพาะเจาะจง เราจะทดสอบวิธีการอัปเดตของบริการ เราจะไม่พิจารณาเลเยอร์ DAO เนื่องจากเราใช้ค่าเริ่มต้น มาเพิ่มการเริ่มต้นสำหรับการทดสอบกันเถอะ:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
และที่นี่เรามีชั้นบริการ:
@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
private final RobotDAO robotDAO;
@Override
public Robot update(Long id, Robot robot) {
Robot found = robotDAO.findById(id);
return robotDAO.update(Robot.builder()
.id(id)
.name(robot.getName() != null ? robot.getName() : found.getName())
.cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
.producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
.build());
}
}
บรรทัดที่ 8 — ดึงวัตถุที่อัพเดตจากฐานข้อมูล บรรทัดที่ 9-14 — สร้างวัตถุผ่านตัวสร้าง หากวัตถุที่เข้ามามีช่องให้ตั้งค่า ถ้าไม่ เราจะปล่อยให้สิ่งที่อยู่ในฐานข้อมูล ตอนนี้ดูการทดสอบของเรา:
@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
@Mock
private RobotDAO robotDAO;
private RobotServiceImpl robotService;
private static Robot testRobot;
@BeforeClass
public static void prepareTestData() {
testRobot = Robot
.builder()
.id(123L)
.name("testRobotMolly")
.cpu("Intel Core i7-9700K")
.producer("China")
.build();
}
@Before
public void init() {
robotService = new RobotServiceImpl(robotDAO);
}
บรรทัดที่ 1 — รองชนะเลิศของเรา บรรทัดที่ 4 — เราแยกบริการออกจากเลเยอร์ DAO โดยแทนที่การจำลอง บรรทัดที่ 11 — เราตั้งค่าเอนทิตีการทดสอบ (อันที่เราจะใช้เป็นหนูตะเภา) สำหรับชั้นเรียน บรรทัดที่ 22 — เราตั้งค่าวัตถุบริการ ซึ่งเป็นสิ่งที่เราจะทดสอบ
@Test
public void updateTest() {
when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
Robot robotForUpdate = Robot
.builder()
.name("Vally")
.cpu("AMD Ryzen 7 2700X")
.build();
Robot resultRobot = robotService.update(123L, robotForUpdate);
assertNotNull(resultRobot);
assertSame(resultRobot.getId(),testRobot.getId());
assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
เราจะเห็นว่าการทดสอบมีสามส่วนที่ชัดเจน: บรรทัดที่ 3-9 — ระบุการแข่งขัน บรรทัดที่ 11 — รันโค้ดภายใต้การทดสอบ บรรทัดที่ 13-17 — ตรวจสอบผลลัพธ์ รายละเอียดเพิ่มเติม: บรรทัดที่ 3-4 — ตั้งค่าลักษณะการทำงานสำหรับการจำลอง DAO บรรทัดที่ 5 — กำหนดอินสแตนซ์ที่เราจะอัปเดตให้เหนือกว่ามาตรฐานของเรา บรรทัดที่ 11 — ใช้วิธีการและรับตัวอย่างผลลัพธ์ บรรทัดที่ 13 — ตรวจสอบว่าไม่เป็นโมฆะ บรรทัดที่ 14 — เปรียบเทียบ ID ของผลลัพธ์และอาร์กิวเมนต์เมธอดที่กำหนด บรรทัดที่ 15 — ตรวจสอบว่ามีการอัปเดตชื่อหรือไม่ บรรทัดที่ 16 — ดูผลลัพธ์ของ CPU บรรทัดที่ 17 — เราไม่ได้ระบุฟิลด์นี้ในอินสแตนซ์ ดังนั้นฟิลด์นี้ควรคงเดิม เราตรวจสอบเงื่อนไขนั้นที่นี่ มาเริ่มกันเลย:ข้อสอบเป็นสีเขียว! เราสามารถถอนหายใจด้วยความโล่งอก :) โดยสรุป การทดสอบช่วยปรับปรุงคุณภาพของโค้ดและทำให้กระบวนการพัฒนามีความยืดหยุ่นและเชื่อถือได้มากขึ้น ลองนึกดูว่าต้องใช้ความพยายามมากแค่ไหนในการออกแบบซอฟต์แวร์ใหม่ที่เกี่ยวข้องกับไฟล์คลาสหลายร้อยไฟล์ เมื่อเรามีการทดสอบหน่วยที่เขียนขึ้นสำหรับคลาสเหล่านี้ทั้งหมด เราสามารถจัดองค์ประกอบใหม่ได้อย่างมั่นใจ และที่สำคัญที่สุดคือช่วยให้เราพบจุดบกพร่องในระหว่างการพัฒนาได้อย่างง่ายดาย ผู้ชายและผู้หญิง นั่นคือทั้งหมดที่ฉันมีในวันนี้ ให้ฉันชอบและแสดงความคิดเห็น :)
GO TO FULL VERSION