เพิ่มความสามารถในการบำรุงรักษาโค้ดด้วยการทดสอบการรวมปฏิกิริยา

เผยแพร่แล้ว: 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 ของฉัน