เพิ่มความสามารถในการบำรุงรักษาโค้ดด้วยการทดสอบการรวมปฏิกิริยา
เผยแพร่แล้ว: 2022-03-11การทดสอบการรวมเป็นจุดที่เหมาะสมระหว่างต้นทุนและมูลค่าของการทดสอบ การเขียนการทดสอบการรวมสำหรับแอป React ด้วยความช่วยเหลือของ react-testing-library แทนหรือนอกเหนือจากการทดสอบหน่วยส่วนประกอบสามารถเพิ่มความสามารถในการบำรุงรักษาโค้ดโดยไม่ทำให้ความเร็วในการพัฒนาลดลง
ในกรณีที่คุณต้องการเริ่มต้นก่อนที่เราจะดำเนินการต่อ คุณสามารถดูตัวอย่างวิธีใช้ react-testing-library สำหรับการทดสอบการรวมแอป React ได้ที่นี่
ทำไมต้องลงทุนในการทดสอบการรวมระบบ?
"การทดสอบการบูรณาการทำให้เกิดความสมดุลอย่างมากในการแลกเปลี่ยนระหว่างความมั่นใจกับความเร็ว/ค่าใช้จ่าย นี่คือเหตุผลที่แนะนำให้ใช้ความพยายามของคุณที่นั่นมากที่สุด (ไม่ใช่ทั้งหมด
– Kent C. Dodds ใน การทดสอบการเขียน ไม่มากเกินไป บูรณาการเป็นส่วนใหญ่
เป็นเรื่องปกติในการเขียนการทดสอบหน่วยสำหรับส่วนประกอบ React ซึ่งมักใช้ไลบรารียอดนิยมสำหรับการทดสอบ React “เอนไซม์”; โดยเฉพาะวิธีการ "ตื้น" วิธีนี้ช่วยให้เราทดสอบส่วนประกอบแยกจากส่วนที่เหลือของแอปได้ อย่างไรก็ตาม เนื่องจากการเขียนแอป React นั้นเกี่ยวกับการเขียนส่วนประกอบ การทดสอบหน่วยเพียงอย่างเดียวจึงไม่รับประกันว่าแอปจะปราศจากข้อบกพร่อง
ตัวอย่างเช่น การเปลี่ยนอุปกรณ์ประกอบฉากที่ยอมรับของส่วนประกอบและการอัปเดตการทดสอบหน่วยที่เกี่ยวข้องอาจส่งผลให้การทดสอบทั้งหมดผ่านในขณะที่แอปอาจยังใช้งานไม่ได้หากไม่มีการอัปเดตส่วนประกอบอื่นตามนั้น
การทดสอบการผสานรวมสามารถช่วยรักษาความอุ่นใจในขณะที่ทำการเปลี่ยนแปลงแอป React เนื่องจากช่วยให้มั่นใจได้ว่าองค์ประกอบของส่วนประกอบจะส่งผลให้เกิด UX ที่ต้องการ
ข้อกำหนดสำหรับการทดสอบการรวมแอพ React
นี่คือสิ่งที่นักพัฒนา React ต้องการ ทำเมื่อเขียนการทดสอบการรวม:
- ทดสอบกรณีการใช้งานแอปพลิเคชันจากมุมมองของผู้ใช้ ผู้ใช้เข้าถึงข้อมูลบนเว็บเพจและโต้ตอบกับการควบคุมที่มีอยู่
- การเรียก Mock API ไม่ขึ้นกับความพร้อมใช้งานของ API และสถานะสำหรับการทดสอบผ่าน/ไม่ผ่าน
- API ของเบราว์เซอร์จำลอง (เช่น ที่เก็บข้อมูลในเครื่อง) เนื่องจากไม่มีอยู่ในสภาพแวดล้อมการทดสอบ
- ยืนยันในสถานะ React DOM (เบราว์เซอร์ DOM หรือสภาพแวดล้อมมือถือดั้งเดิม)
ในตอนนี้ สำหรับบางสิ่งที่เราควรพยายาม หลีกเลี่ยง เมื่อเขียนการทดสอบการรวมแอพ React:
- รายละเอียดการใช้งานทดสอบ การเปลี่ยนแปลงการใช้งานควรหยุดการทดสอบหากมีจุดบกพร่องจริงเท่านั้น
- เยาะเย้ยเกินไป เราต้องการทดสอบว่าทุกส่วนของแอพทำงานร่วมกันอย่างไร
- เรนเดอร์ตื้น เราต้องการทดสอบองค์ประกอบของส่วนประกอบทั้งหมดในแอปจนถึงส่วนประกอบที่เล็กที่สุด
ทำไมต้องเลือก React-testing-library?
ข้อกำหนดดังกล่าวทำให้ห้องสมุดทดสอบปฏิกิริยาเป็นทางเลือกที่ดี เนื่องจากหลักการชี้นำหลักคืออนุญาตให้ส่วนประกอบ React ได้รับการทดสอบในลักษณะที่คล้ายคลึงกับวิธีที่มนุษย์ใช้งานจริง
ไลบรารีพร้อมกับไลบรารีเสริมที่เป็นตัวเลือกช่วยให้เราเขียนการทดสอบที่โต้ตอบกับ DOM และยืนยันสถานะของไลบรารีได้
ตัวอย่างการตั้งค่าแอพ
แอพที่เราจะเขียนตัวอย่างการทดสอบการรวมใช้สถานการณ์ง่ายๆ:
- ผู้ใช้ป้อนชื่อผู้ใช้ GitHub
- แอพแสดงรายการที่เก็บสาธารณะที่เกี่ยวข้องกับชื่อผู้ใช้ที่ป้อน
วิธีการใช้งานฟังก์ชันข้างต้นจะไม่เกี่ยวข้องจากมุมมองของการทดสอบการรวม อย่างไรก็ตาม เพื่อให้ใกล้เคียงกับแอปพลิเคชันในโลกแห่งความเป็นจริง แอปจึงปฏิบัติตามรูปแบบ React ทั่วไป ดังนั้นแอป:
- เป็นแอปหน้าเดียว (SPA)
- ทำการร้องขอ API
- มีการจัดการของรัฐทั่วโลก
- รองรับความเป็นสากล
- ใช้ไลบรารีส่วนประกอบ React
สามารถดูซอร์สโค้ดสำหรับการใช้งานแอพได้ที่นี่
การเขียนแบบทดสอบบูรณาการ
การติดตั้งการพึ่งพา
ด้วยเส้นด้าย:
yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock
หรือด้วย npm:
npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock
การสร้างไฟล์ชุดทดสอบการรวม
เราจะสร้างไฟล์ชื่อไฟล์ viewGitHubRepositoriesByUsername.spec.js
ในโฟลเดอร์ . ./test
ของแอปพลิเคชันของเรา Jest จะหยิบมันขึ้นมาโดยอัตโนมัติ
การนำเข้าการพึ่งพาในไฟล์ทดสอบ
import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test
การตั้งค่าห้องทดสอบ
describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });
หมายเหตุ:
- ก่อนการทดสอบทั้งหมด ให้จำลอง GitHub API เพื่อส่งคืนรายการที่เก็บเมื่อเรียกด้วยชื่อผู้ใช้เฉพาะ
- หลังจากการทดสอบแต่ละครั้ง ให้ทำความสะอาดการทดสอบ React DOM เพื่อให้การทดสอบแต่ละครั้งเริ่มจากจุดที่สะอาด
-
describe
บล็อค ระบุกรณีการใช้งานการทดสอบการรวมและรูปแบบการไหล - รูปแบบการไหลที่เรากำลังทดสอบคือ:
- ผู้ใช้ป้อนชื่อผู้ใช้ที่ถูกต้องซึ่งเชื่อมโยงกับที่เก็บ GitHub สาธารณะ
- ผู้ใช้ป้อนชื่อผู้ใช้ที่ถูกต้องซึ่งไม่มีที่เก็บ GitHub สาธารณะที่เกี่ยวข้อง
- ผู้ใช้ป้อนชื่อผู้ใช้ที่ไม่มีอยู่ใน GitHub
-
it
บล็อกการใช้ async callback เนื่องจากกรณีการใช้งานที่พวกเขากำลังทดสอบมีขั้นตอนแบบอะซิงโครนัสอยู่ในนั้น
การเขียนการทดสอบการไหลครั้งแรก
ขั้นแรก แอปจะต้องแสดงผล
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
วิธีการ render
นเดอร์ที่นำเข้าจากโมดูล @testing-library/react
แสดงผลแอปในการทดสอบ React DOM และส่งคืนเคียวรี DOM ที่ผูกกับคอนเทนเนอร์แอปที่แสดงผล แบบสอบถามเหล่านี้ใช้เพื่อค้นหาองค์ประกอบ DOM เพื่อโต้ตอบและยืนยัน
ตอนนี้ ในขั้นแรกของโฟลว์ภายใต้การทดสอบ ผู้ใช้จะได้รับฟิลด์ชื่อผู้ใช้และพิมพ์สตริงชื่อผู้ใช้ลงไป
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
ตัวช่วย userEvent
จากโมดูล @testing-library/user-event
นำเข้ามีวิธี type
ที่เลียนแบบพฤติกรรมของผู้ใช้เมื่อพิมพ์ข้อความลงในช่องข้อความ ยอมรับสองพารามิเตอร์: องค์ประกอบ DOM ที่ยอมรับอินพุตและสตริงที่ผู้ใช้พิมพ์

ผู้ใช้มักจะพบองค์ประกอบ DOM จากข้อความที่เกี่ยวข้อง ในกรณีของอินพุต จะเป็นข้อความป้ายกำกับหรือข้อความตัวแทน วิธีการสืบค้น getByPlaceholderText
ที่ส่งคืนก่อนหน้านี้จากการ render
นเดอร์ช่วยให้เราค้นหาองค์ประกอบ DOM ด้วยข้อความตัวยึดตำแหน่ง
โปรดทราบว่าเนื่องจากตัวข้อความมักจะมีการเปลี่ยนแปลง ทางที่ดีไม่ควรพึ่งพาค่าการแปลเป็นภาษาท้องถิ่นจริง และกำหนดค่าโมดูลการแปลให้ส่งคืนคีย์รายการการแปลเป็นค่าแทน
ตัวอย่างเช่น เมื่อการโลคัลไลเซชัน “en-US” ปกติจะส่งคืน Enter GitHub username
เป็นค่าสำหรับคีย์ userSelection.usernamePlaceholder
ในการทดสอบ เราต้องการให้ส่งคืน userSelection.usernamePlaceholder
เมื่อผู้ใช้พิมพ์ข้อความลงในฟิลด์ พวกเขาควรเห็นค่าฟิลด์ข้อความที่อัปเดต
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
ถัดไปในโฟลว์ ผู้ใช้คลิกปุ่มส่งและคาดว่าจะเห็นรายการที่เก็บ
userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');
เมธอด userEvent.click
เลียนแบบผู้ใช้คลิกที่องค์ประกอบ DOM ในขณะที่การสืบค้น getByText
ค้นหาองค์ประกอบ DOM ด้วยข้อความที่มีอยู่ ตัวปรับแต่ง closest
ช่วยให้แน่ใจว่าเราเลือกองค์ประกอบที่เหมาะสมที่สุด
หมายเหตุ: ในการทดสอบการรวม ขั้นตอนมักใช้ทั้งบทบาท act
และการ assert
ตัวอย่างเช่น เรายืนยันว่าผู้ใช้สามารถคลิกปุ่มโดยคลิกได้
ในขั้นตอนก่อนหน้านี้ เรายืนยันว่าผู้ใช้เห็นส่วนรายการที่เก็บของแอป ตอนนี้ เราต้องยืนยันว่าเนื่องจากการดึงรายการที่เก็บจาก GitHub อาจใช้เวลาสักครู่ ผู้ใช้จะเห็นข้อบ่งชี้ว่ากำลังดึงข้อมูลอยู่ เราต้องการให้แน่ใจว่าแอพไม่ได้บอกผู้ใช้ว่าไม่มีที่เก็บที่เกี่ยวข้องกับชื่อผู้ใช้ที่ป้อนในขณะที่รายการที่เก็บยังคงถูกดึงข้อมูล
getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();
โปรดทราบว่าคำนำหน้าข้อความค้นหา getBy
ใช้เพื่อยืนยันว่าสามารถพบองค์ประกอบ DOM ได้ และคำนำหน้าข้อความค้นหา queryBy
มีประโยชน์สำหรับการยืนยันที่ตรงกันข้าม นอกจากนี้ queryBy
จะไม่ส่งคืนข้อผิดพลาดหากไม่พบองค์ประกอบ
ต่อไป เราต้องการให้แน่ใจว่าในที่สุดแอปจะดึงที่เก็บข้อมูลเสร็จแล้วและแสดงให้ผู้ใช้เห็น
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));
waitForElement
แบบอะซิงโครนัสใช้เพื่อรอการอัปเดต DOM ที่จะแสดงการยืนยันที่ระบุเป็นพารามิเตอร์ของเมธอด ในกรณีนี้ เราขอยืนยันว่าแอปแสดงชื่อและคำอธิบายสำหรับที่เก็บทั้งหมดที่ส่งคืนโดย GitHub API ที่จำลอง
สุดท้าย แอปไม่ควรแสดงตัวบ่งชี้ว่ากำลังดึงข้อมูลที่เก็บอีกต่อไป และไม่ควรแสดงข้อความแสดงข้อผิดพลาด
expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();
ผลการทดสอบการรวม React ของเรามีลักษณะดังนี้:
it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });
การทดสอบการไหลสำรอง
เมื่อผู้ใช้ป้อนชื่อผู้ใช้ GitHub โดยไม่มีที่เก็บสาธารณะที่เกี่ยวข้อง แอปจะแสดงข้อความที่เหมาะสม
describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });
เมื่อผู้ใช้ป้อนชื่อผู้ใช้ GitHub ที่ไม่มีอยู่ แอปจะแสดงข้อความแสดงข้อผิดพลาด
describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });
เหตุใด React Integration จึงทดสอบ Rock
การทดสอบการผสานรวมถือเป็นจุดที่น่าสนใจสำหรับแอปพลิเคชัน React อย่างแท้จริง การทดสอบเหล่านี้ช่วยตรวจจับจุดบกพร่องและใช้วิธี TDD ในขณะเดียวกันก็ไม่จำเป็นต้องบำรุงรักษาเมื่อมีการเปลี่ยนแปลงการใช้งาน
React-testing-library ที่แสดงในบทความนี้เป็นเครื่องมือที่ยอดเยี่ยมสำหรับการเขียนการทดสอบการรวม React เนื่องจากช่วยให้คุณสามารถโต้ตอบกับแอปได้เหมือนกับที่ผู้ใช้ทำ และตรวจสอบสถานะและพฤติกรรมของแอปจากมุมมองของผู้ใช้
หวังว่าตัวอย่างที่ให้ไว้ที่นี่จะช่วยให้คุณเริ่มเขียนการทดสอบการรวมในโครงการ React ใหม่และที่มีอยู่ โค้ดตัวอย่างทั้งหมดที่มีการปรับใช้แอปสามารถดูได้ที่ GitHub ของฉัน