การพัฒนา React.js ที่ขับเคลื่อนด้วยการทดสอบ: การทดสอบหน่วย React.js ด้วยเอนไซม์และ Jest
เผยแพร่แล้ว: 2022-03-11โค้ดใดๆ ที่ไม่มีการทดสอบใดๆ ถือเป็นรหัสดั้งเดิม ตามที่ Michael Feathers กล่าว ดังนั้น หนึ่งในวิธีที่ดีที่สุดในการหลีกเลี่ยงการสร้างโค้ดเดิมคือการใช้การพัฒนาที่ขับเคลื่อนด้วยการทดสอบ (TDD)
แม้ว่าจะมีเครื่องมือมากมายสำหรับการทดสอบหน่วย JavaScript และ React.js ในโพสต์นี้ เราจะใช้ Jest และ Enzyme เพื่อสร้างส่วนประกอบ React.js พร้อมฟังก์ชันพื้นฐานโดยใช้ TDD
เหตุใดจึงต้องใช้ TDD เพื่อสร้างส่วนประกอบ React.js
TDD นำประโยชน์มากมายมาสู่โค้ดของคุณ—ข้อดีอย่างหนึ่งของการครอบคลุมการทดสอบในระดับสูงคือช่วยให้สร้างโค้ดใหม่ได้ง่ายในขณะที่รักษาโค้ดของคุณให้สะอาดและทำงานได้
หากคุณเคยสร้างองค์ประกอบ React.js มาก่อน คุณจะรู้ว่าโค้ดสามารถเติบโตได้เร็วมาก มันเต็มไปด้วยเงื่อนไขที่ซับซ้อนมากมายที่เกิดจากคำสั่งที่เกี่ยวข้องกับการเปลี่ยนแปลงสถานะและการเรียกใช้บริการ
ทุกองค์ประกอบที่ไม่มีการทดสอบหน่วยมีรหัสดั้งเดิมที่ยากต่อการบำรุงรักษา เราสามารถเพิ่มการทดสอบหน่วยหลังจากที่เราสร้างรหัสการผลิต อย่างไรก็ตาม เราอาจเสี่ยงต่อการมองข้ามบางสถานการณ์ที่ควรได้รับการทดสอบ การสร้างการทดสอบก่อนทำให้เรามีโอกาสสูงที่จะครอบคลุมทุกสถานการณ์ตรรกะในองค์ประกอบของเรา ซึ่งจะทำให้ง่ายต่อการปรับโครงสร้างและบำรุงรักษา
เราจะทดสอบส่วนประกอบ React.js ได้อย่างไร?
มีกลยุทธ์มากมายที่เราสามารถใช้ทดสอบส่วนประกอบ React.js:
- เราสามารถตรวจสอบได้ว่ามีการเรียกฟังก์ชันเฉพาะใน
props
เมื่อมีการส่งเหตุการณ์บางอย่าง - นอกจากนี้เรายังสามารถรับผลลัพธ์ของฟังก์ชันการ
render
นเดอร์ตามสถานะของคอมโพเนนต์ปัจจุบันและจับคู่กับเลย์เอาต์ที่กำหนดไว้ล่วงหน้า - เรายังสามารถตรวจสอบได้ว่าจำนวนของส่วนประกอบย่อยตรงกับปริมาณที่คาดไว้หรือไม่
เพื่อที่จะใช้กลยุทธ์เหล่านี้ เราจะใช้เครื่องมือสองอย่างที่มีประโยชน์ในการทำงานกับการทดสอบใน React.js: Jest และ Enzyme
ใช้ Jest เพื่อสร้าง Unit Tests
Jest เป็นเฟรมเวิร์กการทดสอบโอเพ่นซอร์สที่สร้างโดย Facebook ที่มีการบูรณาการที่ยอดเยี่ยมกับ React.js ประกอบด้วยเครื่องมือบรรทัดคำสั่งสำหรับการทดสอบคล้ายกับที่จัสมินและมอคค่าเสนอ นอกจากนี้ยังช่วยให้เราสร้างฟังก์ชันจำลองที่มีการกำหนดค่าเกือบเป็นศูนย์และมีชุดตัวจับคู่ที่ดีจริงๆ ที่ทำให้การยืนยันอ่านง่ายขึ้น
นอกจากนี้ยังมีคุณสมบัติที่ดีมากที่เรียกว่า “การทดสอบสแนปชอต” ซึ่งช่วยให้เราตรวจสอบและตรวจสอบผลการเรนเดอร์ส่วนประกอบ เราจะใช้การทดสอบสแนปชอตเพื่อจับภาพแผนผังของส่วนประกอบและบันทึกลงในไฟล์ที่เราสามารถใช้เปรียบเทียบกับแผนผังการแสดงผล (หรืออะไรก็ตามที่เราส่งผ่านไปยังฟังก์ชันที่ expect
เป็นอาร์กิวเมนต์แรก)
การใช้ Enzyme เพื่อ Mount React.js Components
เอนไซม์มีกลไกในการติดตั้งและสำรวจแผนผังส่วนประกอบ React.js ซึ่งจะช่วยให้เราเข้าถึงคุณสมบัติและสถานะของตนเอง รวมถึงอุปกรณ์ประกอบฉากย่อยเพื่อดำเนินการยืนยันของเรา
เอ็นไซม์มีฟังก์ชันพื้นฐานสองประการสำหรับการติดตั้งส่วนประกอบ: แบบ shallow
และ mount
ฟังก์ชัน shallow
จะโหลดในหน่วยความจำเฉพาะคอมโพเนนต์รูทในขณะที่การ mount
นต์โหลดแผนผัง DOM แบบเต็ม
เราจะรวม Enzyme และ Jest เพื่อเมาต์ส่วนประกอบ React.js และเรียกใช้การยืนยัน
การตั้งค่าสภาพแวดล้อมของเรา
คุณสามารถดู repo นี้ ซึ่งมีการกำหนดค่าพื้นฐานเพื่อเรียกใช้ตัวอย่างนี้
เรากำลังใช้เวอร์ชันต่อไปนี้:
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }
การสร้างส่วนประกอบ React.js โดยใช้ TDD
ขั้นตอนแรกคือการสร้างการทดสอบที่ล้มเหลวซึ่งจะพยายามแสดงส่วนประกอบ React.js โดยใช้ฟังก์ชันตื้นของเอนไซม์
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });
หลังจากรันการทดสอบ เราได้รับข้อผิดพลาดดังต่อไปนี้:
ReferenceError: MyComponent is not defined.
จากนั้นเราจะสร้างส่วนประกอบที่มีไวยากรณ์พื้นฐานเพื่อให้ผ่านการทดสอบ
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }
ในขั้นตอนต่อไป เราจะตรวจสอบให้แน่ใจว่าส่วนประกอบของเราแสดงเค้าโครง UI ที่กำหนดไว้ล่วงหน้าโดยใช้ฟังก์ชัน toMatchSnapshot
จาก Jest
หลังจากเรียกใช้เมธอดนี้ Jest จะสร้างไฟล์สแน็ปช็อตชื่อ [testFileName].snap
โดยอัตโนมัติ ซึ่งเพิ่มโฟลเดอร์ __snapshots__
ไฟล์นี้แสดงถึงเลย์เอาต์ UI ที่เราคาดหวังจากการแสดงผลคอมโพเนนต์ของเรา
อย่างไรก็ตาม เนื่องจากเรากำลังพยายามทำ TDD ล้วนๆ เราควรสร้างไฟล์นี้ก่อนแล้วจึงเรียกใช้ฟังก์ชัน toMatchSnapshot
เพื่อให้การทดสอบล้มเหลว
นี้อาจฟังดูสับสนเล็กน้อย เนื่องจากเราไม่ทราบว่ารูปแบบใดที่ Jest ใช้เพื่อแสดงเลย์เอาต์นี้
คุณอาจถูกล่อลวงให้เรียกใช้ฟังก์ชัน toMatchSnapshot
ก่อน และดูผลลัพธ์ในไฟล์สแน็ปช็อต และนั่นก็เป็นตัวเลือกที่ถูกต้อง อย่างไรก็ตาม หากเราต้องการใช้ TDD แท้จริง เราต้องเรียนรู้ว่าไฟล์สแน็ปช็อตมีโครงสร้างอย่างไร
ไฟล์สแน็ปช็อตมีเลย์เอาต์ที่ตรงกับชื่อการทดสอบ ซึ่งหมายความว่าหากการทดสอบของเรามีแบบฟอร์มนี้:
desc("ComponentA" () => { it("should do something", () => { … } });
เราควรระบุสิ่งนี้ในส่วนการส่งออก: Component A should do something 1
.
คุณสามารถอ่านเพิ่มเติมเกี่ยวกับการทดสอบสแนปชอตได้ที่นี่
ดังนั้นเราจึงสร้างไฟล์ MyComponent.test.js.snap
ขึ้นมาก่อน
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;
จากนั้น เราสร้างการทดสอบหน่วยที่จะตรวจสอบว่าสแน็ปช็อตตรงกับองค์ประกอบลูกขององค์ประกอบ
// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...
เราสามารถพิจารณา components.getElements
อันเป็นผลมาจากวิธีการเรนเดอร์

เราส่งองค์ประกอบเหล่านี้ไปยังวิธีที่ expect
เพื่อเรียกใช้การตรวจสอบกับไฟล์สแน็ปช็อต
หลังจากดำเนินการทดสอบ เราได้รับข้อผิดพลาดดังต่อไปนี้:
Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []
Jest กำลังบอกเราว่าผลลัพธ์จาก component.getElements
ไม่ตรงกับสแน็ปช็อต ดังนั้นเราจึงทำให้การทดสอบนี้ผ่านโดยการเพิ่มองค์ประกอบอินพุตใน MyComponent
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }
ขั้นตอนต่อไปคือการเพิ่มฟังก์ชันการทำงานให้กับ input
โดยเรียกใช้ฟังก์ชันเมื่อค่าของฟังก์ชันเปลี่ยนไป เราทำได้โดยการระบุฟังก์ชันในอุปกรณ์ onChange
ก่อนอื่นเราต้องเปลี่ยนสแน็ปช็อตเพื่อให้การทดสอบล้มเหลว
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;
ข้อเสียของการแก้ไขสแน็ปช็อตก่อนคือลำดับของอุปกรณ์ประกอบฉาก (หรือแอตทริบิวต์) มีความสำคัญ
Jest จะจัดเรียงอุปกรณ์ประกอบฉากที่ได้รับในฟังก์ชัน expect
ตามลำดับตัวอักษร ก่อนตรวจสอบกับสแน็ปช็อต ดังนั้นเราควรระบุตามลำดับนั้น
หลังจากดำเนินการทดสอบ เราได้รับข้อผิดพลาดดังต่อไปนี้:
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]
เพื่อให้การทดสอบนี้ผ่าน เราสามารถจัดเตรียมฟังก์ชันว่างให้กับ onChange
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }
จากนั้น เราตรวจสอบให้แน่ใจว่าสถานะของส่วนประกอบเปลี่ยนแปลงหลังจากส่งเหตุการณ์ onChange
ในการทำเช่นนี้ เราสร้างการทดสอบหน่วยใหม่ซึ่งจะเรียกใช้ onChange
ในอินพุตโดยผ่าน เหตุการณ์ เพื่อเลียนแบบเหตุการณ์จริงใน UI
จากนั้น เราตรวจสอบว่า สถานะ คอมโพเนนต์มีคีย์ชื่อ input
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });
ตอนนี้เราได้รับข้อผิดพลาดดังต่อไปนี้
Expected value to be defined, instead received undefined
สิ่งนี้บ่งชี้ว่าส่วนประกอบไม่มีคุณสมบัติในสถานะที่เรียกว่า input
เราทำการทดสอบโดยการตั้งค่ารายการนี้ในสถานะของส่วนประกอบ
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }
จากนั้น เราต้องตรวจสอบให้แน่ใจว่าได้ตั้งค่าในรายการสถานะใหม่ เราจะได้ค่านี้จากการจัดงาน
มาสร้างการทดสอบเพื่อให้แน่ใจว่าสถานะมีค่านี้
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""
ในที่สุดเราก็ทำให้การทดสอบนี้ผ่านโดยรับค่าจากเหตุการณ์และตั้งค่าเป็นค่าอินพุต
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }
หลังจากที่แน่ใจว่าการทดสอบทั้งหมดผ่าน เราสามารถจัดโครงสร้างโค้ดของเราใหม่ได้
เราสามารถแยกฟังก์ชันที่ส่งผ่านใน onChange
prop ไปยังฟังก์ชันใหม่ที่เรียกว่า updateState
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }
ตอนนี้เรามีส่วนประกอบ React.js อย่างง่ายที่สร้างโดยใช้ TDD
สรุป
ในตัวอย่างนี้ เราพยายามใช้ TDD ล้วนๆ โดยทำตามทุกขั้นตอนโดยเขียนโค้ดที่น้อยที่สุดเท่าที่จะเป็นไปได้ที่จะล้มเหลวและผ่านการทดสอบ
บางขั้นตอนอาจดูเหมือนไม่จำเป็นและเราอาจถูกล่อลวงให้ข้ามไป อย่างไรก็ตาม เมื่อใดก็ตามที่เราข้ามขั้นตอนใดๆ เราจะลงเอยด้วยการใช้ TDD เวอร์ชัน ที่บริสุทธิ์น้อยกว่า
การใช้กระบวนการ TDD ที่เข้มงวดน้อยกว่านั้นก็ใช้ได้และอาจทำงานได้ดี
คำแนะนำของฉันสำหรับคุณคือหลีกเลี่ยงการข้ามขั้นตอนใดๆ และอย่ารู้สึกแย่หากคุณพบว่ามันยาก TDD เป็นเทคนิคที่ไม่ง่ายที่จะเชี่ยวชาญ แต่ก็คุ้มค่าที่จะทำ
หากคุณสนใจที่จะเรียนรู้เพิ่มเติมเกี่ยวกับ TDD และการพัฒนาที่ขับเคลื่อนด้วยพฤติกรรมที่เกี่ยวข้อง (BDD) ให้อ่าน เจ้านายของคุณจะไม่ชื่นชม TDD โดยเพื่อน Toptaler Ryan Wilcox