ส่วนของรหัสใด ๆ ที่ไม่มีการทดสอบจะกล่าวว่าเป็นรหัสเดิมตาม Michael Feathers ดังนั้นวิธีที่ดีที่สุดวิธีหนึ่งในการหลีกเลี่ยงการสร้างรหัสเดิมคือการใช้การทดสอบขับเคลื่อนการพัฒนา (TDD)
แม้ว่าจะมีเครื่องมือมากมายสำหรับ JavaScript และ React.js การทดสอบหน่วยในโพสต์นี้เราจะใช้ Jest และ Enzyme เพื่อสร้างส่วนประกอบ React.js ด้วยฟังก์ชันพื้นฐานโดยใช้ TDD
TDD มอบประโยชน์มากมายให้กับโค้ดของคุณข้อดีอย่างหนึ่งของการครอบคลุมการทดสอบที่สูงคือช่วยให้สามารถ refactoring โค้ดได้ง่ายในขณะที่ทำให้โค้ดของคุณสะอาดและใช้งานได้
หากคุณเคยสร้างคอมโพเนนต์ React.js มาก่อนคุณได้ตระหนักว่าโค้ดสามารถเติบโตได้อย่างรวดเร็ว มันเต็มไปด้วยเงื่อนไขที่ซับซ้อนมากมายที่เกิดจากคำสั่งที่เกี่ยวข้องกับการเปลี่ยนแปลงสถานะและการเรียกใช้บริการ
ทุกส่วนประกอบที่ขาดการทดสอบหน่วยมีรหัสเดิมที่ยากต่อการบำรุงรักษา เราสามารถเพิ่มการทดสอบหน่วยได้หลังจากที่เราสร้างรหัสการผลิต อย่างไรก็ตามเราอาจเสี่ยงต่อการมองข้ามสถานการณ์บางอย่างที่ควรได้รับการทดสอบ โดยการสร้างการทดสอบก่อนเรามีโอกาสสูงกว่าที่จะครอบคลุมทุกสถานการณ์ตรรกะในส่วนประกอบของเราซึ่งจะทำให้ง่ายต่อการ refactor และบำรุงรักษา
มีหลายกลยุทธ์ที่เราสามารถใช้เพื่อทดสอบส่วนประกอบ React.js:
props
ถูกเรียกเมื่อมีการส่งเหตุการณ์บางอย่างrender
ฟังก์ชันกำหนดสถานะขององค์ประกอบปัจจุบันและจับคู่กับเค้าโครงที่กำหนดไว้ล่วงหน้าในการใช้กลยุทธ์เหล่านี้เราจะใช้เครื่องมือสองอย่างที่มีประโยชน์ในการทำงานกับการทดสอบใน React.js: คือ และ เอนไซม์ .
เหตุใดฉันจึงควรใช้ node.js
Jest เป็นกรอบการทดสอบโอเพ่นซอร์สที่สร้างโดย Facebook ซึ่งมีการทำงานร่วมกับ React.js อย่างดีเยี่ยม มีเครื่องมือบรรทัดคำสั่งสำหรับการดำเนินการทดสอบที่คล้ายกับข้อเสนอของจัสมินและมอคค่า นอกจากนี้ยังช่วยให้เราสามารถสร้างฟังก์ชันจำลองที่มีการกำหนดค่าเกือบเป็นศูนย์และมีชุดตัวจับคู่ที่ดีมากซึ่งทำให้การยืนยันอ่านง่ายขึ้น
นอกจากนี้ยังมีคุณสมบัติที่ดีมากที่เรียกว่า 'การทดสอบภาพรวม' ซึ่งช่วยให้เราตรวจสอบและยืนยันผลการแสดงผลส่วนประกอบ เราจะใช้การทดสอบสแนปชอตเพื่อจับโครงสร้างขององค์ประกอบและบันทึกลงในไฟล์ที่เราสามารถใช้เพื่อเปรียบเทียบกับแผนภูมิการแสดงผล (หรืออะไรก็ตามที่เราส่งผ่านไปยังฟังก์ชัน expect
เป็นอาร์กิวเมนต์แรก)
Enzyme มีกลไกในการติดตั้งและสำรวจทรีส่วนประกอบ 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 Component โดยใช้ฟังก์ชันตื้นของเอนไซม์
// 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(); }); });
หลังจากทำการทดสอบเราได้รับข้อผิดพลาดต่อไปนี้:
ReferenceError: MyComponent is not defined.
จากนั้นเราจะสร้างส่วนประกอบที่จัดเตรียมไวยากรณ์พื้นฐานเพื่อให้การทดสอบผ่าน
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return ; } }
ในขั้นตอนต่อไปเราจะตรวจสอบให้แน่ใจว่าองค์ประกอบของเราแสดงเค้าโครง 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 [ , ] `;
จากนั้นสร้างการทดสอบหน่วยที่จะตรวจสอบว่าสแนปชอตตรงกับองค์ประกอบลูกของคอมโพเนนต์
// MyComponent.test.js ... it('should render initial layout', () => { // when const component = shallow(); // then expect(component.getElements()).toMatchSnapshot(); }); ...
เราสามารถพิจารณา components.getElements
อันเป็นผลมาจากวิธีการแสดงผล
เราส่งผ่านองค์ประกอบเหล่านี้ไปยัง expect
เพื่อรันการตรวจสอบกับไฟล์สแนปชอต
หลังจากดำเนินการทดสอบเราได้รับข้อผิดพลาดต่อไปนี้:
Received value does not match stored snapshot 1. Expected: - Array [ , ] Actual: + Array []
Jest กำลังบอกเราว่าผลลัพธ์จาก component.getElements
ไม่ตรงกับสแนปชอต ดังนั้นเราจึงทำให้การทดสอบนี้ผ่านไปโดยการเพิ่มองค์ประกอบอินพุตใน MyComponent
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return ; } }
ขั้นตอนต่อไปคือการเพิ่มฟังก์ชันให้กับ input
โดยเรียกใช้ฟังก์ชันเมื่อค่าของมันเปลี่ยนไป เราทำได้โดยระบุฟังก์ชันใน onChange
เสา
ก่อนอื่นเราต้องเปลี่ยนภาพรวมเพื่อให้การทดสอบล้มเหลว
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ , ] `;
ข้อเสียของการแก้ไขสแน็ปช็อตอันดับแรกคือลำดับของอุปกรณ์ประกอบฉาก (หรือแอตทริบิวต์) มีความสำคัญ
Jest จะเรียงอุปกรณ์ประกอบฉากที่ได้รับตามตัวอักษรใน expect
ก่อนที่จะตรวจสอบกับสแนปชอต ดังนั้นเราควรระบุตามลำดับนั้น
หลังจากดำเนินการทดสอบเราได้รับข้อผิดพลาดต่อไปนี้:
Received value does not match stored snapshot 1. Expected: - Array [ onChange={[Function]} , ] Actual: + Array [ , ]
เพื่อให้การทดสอบนี้ผ่านเราสามารถจัดเตรียมฟังก์ชันว่างให้กับ onChange
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return {}} type='text' /> ; } }
จากนั้นตรวจสอบให้แน่ใจว่าสถานะของคอมโพเนนต์เปลี่ยนไปหลังจาก onChange
เหตุการณ์ถูกส่งไป
อึก vs grunt vs webpack
ในการดำเนินการนี้เราจะสร้างการทดสอบหน่วยใหม่ซึ่งจะเรียกว่า onChange
ฟังก์ชันในอินพุตโดยส่งไฟล์ เหตุการณ์ เพื่อเลียนแบบเหตุการณ์จริงใน UI
จากนั้นเราจะตรวจสอบว่าส่วนประกอบนั้น สถานะ มีคีย์ชื่อ input
// MyComponent.test.js ... it('should create an entry in component state', () => { // given const component = shallow(); 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 {this.setState({input: ''})}} type='text' /> ; } }
จากนั้นเราต้องแน่ใจว่ามีการตั้งค่าในรายการสถานะใหม่ เราจะได้ค่านี้จากเหตุการณ์
ลองสร้างการทดสอบที่ทำให้แน่ใจว่าสถานะมีค่านี้
// MyComponent.test.js ... it('should create an entry in component state with the event value', () => { // given const component = shallow(); 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 { this.setState({input: event.target.value})}} type='text' /> ; } }
หลังจากแน่ใจว่าการทดสอบทั้งหมดผ่านแล้วเราสามารถเปลี่ยนรหัสของเราใหม่ได้
เราสามารถแยกฟังก์ชั่นที่ส่งผ่านใน onChange
สนับสนุนฟังก์ชันใหม่ที่เรียกว่า updateState
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return ; } }
ตอนนี้เรามีคอมโพเนนต์ React.js ง่ายๆที่สร้างโดยใช้ TDD
โปรแกรมที่เขียนด้วย c++
ในตัวอย่างนี้เราพยายามใช้ไฟล์ บริสุทธิ์ TDD โดยทำตามทุกขั้นตอนการเขียนโค้ดน้อยที่สุดที่เป็นไปได้ที่จะล้มเหลวและผ่านการทดสอบ
ขั้นตอนบางอย่างอาจดูเหมือนไม่จำเป็นและเราอาจถูกล่อลวงให้ข้ามไป อย่างไรก็ตามเมื่อใดก็ตามที่เราข้ามขั้นตอนใด ๆ เราจะใช้ a บริสุทธิ์น้อย รุ่น TDD
การใช้กระบวนการ TDD ที่เข้มงวดน้อยกว่าก็ใช้ได้เช่นกันและอาจใช้งานได้ดี
คำแนะนำของฉันสำหรับคุณคือหลีกเลี่ยงการข้ามขั้นตอนใด ๆ และอย่ารู้สึกแย่หากคุณพบว่ามันยาก TDD เป็นเทคนิคที่ไม่ง่ายที่จะเชี่ยวชาญ แต่ก็คุ้มค่าที่จะทำ
หากคุณสนใจที่จะเรียนรู้เพิ่มเติมเกี่ยวกับ TDD และการพัฒนาที่ขับเคลื่อนด้วยพฤติกรรม (BDD) โปรดอ่าน เจ้านายของคุณจะไม่ชื่นชม TDD โดยเพื่อน ApeeScapeer Ryan Wilcox
การพัฒนาที่ขับเคลื่อนด้วยการทดสอบเป็นกระบวนการพัฒนาซอฟต์แวร์ตามลำดับที่เราเขียนรหัสทดสอบและการผลิต โดยสรุปเราต้องการให้น้อยที่สุดทั้งรหัสทดสอบที่จำเป็นในการล้มเหลวและรหัสการผลิตที่จำเป็นในการผ่าน
องค์ประกอบการตอบสนองรวมตรรกะและรหัสการนำเสนอ ส่วนใหญ่จะใช้เพื่อจัดเตรียมเลเยอร์นามธรรมสำหรับส่วนประกอบ UI ของเว็บและอุปกรณ์เคลื่อนที่ตามสถานะและคุณสมบัติที่เปลี่ยนแปลง