การพัฒนา 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 และเรียกใช้การยืนยัน

ขั้นตอน TDD เพื่อสร้างส่วนประกอบตอบสนอง

การตั้งค่าสภาพแวดล้อมของเรา

คุณสามารถดู 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