การเขียนโค้ดที่ทดสอบได้ใน JavaScript: ภาพรวมโดยย่อ

เผยแพร่แล้ว: 2022-03-11

ไม่ว่าเราจะใช้ Node จับคู่กับเฟรมเวิร์กการทดสอบอย่าง Mocha หรือ Jasmine หรือการทดสอบที่ขึ้นกับ DOM ในเบราว์เซอร์แบบ headless เช่น PhantomJS ตัวเลือกสำหรับการทดสอบหน่วย JavaScript ของเราก็ดีขึ้นกว่าที่เคย

อย่างไรก็ตาม นี่ไม่ได้หมายความว่าโค้ดที่เรากำลังทดสอบนั้นใช้งานง่ายเหมือนเครื่องมือของเรา! การจัดระเบียบและการเขียนโค้ดที่ทดสอบได้ง่ายนั้นต้องใช้ความพยายามและการวางแผน แต่มีรูปแบบบางส่วนซึ่งได้รับแรงบันดาลใจจากแนวคิดการเขียนโปรแกรมเชิงฟังก์ชัน ที่เราสามารถใช้เพื่อหลีกเลี่ยงปัญหาเมื่อถึงเวลาทดสอบโค้ดของเรา ในบทความนี้ เราจะพูดถึงเคล็ดลับและรูปแบบที่เป็นประโยชน์สำหรับการเขียนโค้ดที่ทดสอบได้ใน JavaScript

แยกตรรกะทางธุรกิจและตรรกะการแสดงผลออกจากกัน

งานหลักอย่างหนึ่งของแอปพลิเคชันเบราว์เซอร์ที่ใช้ JavaScript คือการฟังเหตุการณ์ DOM ที่เรียกใช้โดยผู้ใช้ปลายทาง จากนั้นจึงตอบสนองต่อเหตุการณ์เหล่านี้โดยเรียกใช้ตรรกะทางธุรกิจและแสดงผลลัพธ์บนหน้า การเขียนฟังก์ชันที่ไม่ระบุชื่อซึ่งทำงานจำนวนมากในตำแหน่งที่คุณกำลังตั้งค่าผู้ฟังเหตุการณ์ DOM เป็นเรื่องที่น่าดึงดูดใจ ปัญหาที่เกิดขึ้นคือตอนนี้คุณต้องจำลองเหตุการณ์ DOM เพื่อทดสอบฟังก์ชันที่ไม่ระบุตัวตนของคุณ ซึ่งสามารถสร้างโอเวอร์เฮดได้ทั้งในบรรทัดโค้ดและเวลาที่ใช้ในการทดสอบ

ให้เขียนฟังก์ชันที่มีชื่อแล้วส่งต่อไปยังตัวจัดการเหตุการณ์แทน ด้วยวิธีนี้ คุณจะเขียนการทดสอบสำหรับฟังก์ชันที่มีชื่อได้โดยตรงและไม่ต้องกระโดดข้ามห่วงเพื่อทริกเกอร์เหตุการณ์ DOM ปลอม

สิ่งนี้ใช้ได้กับมากกว่า DOM API จำนวนมากทั้งในเบราว์เซอร์และใน Node ได้รับการออกแบบมาให้เริ่มทำงานและรับฟังเหตุการณ์หรือรอให้งานอะซิงโครนัสประเภทอื่นๆ เสร็จสมบูรณ์ หลักการทั่วไปคือ หากคุณกำลังเขียนฟังก์ชันการโทรกลับแบบไม่ระบุชื่อจำนวนมาก รหัสของคุณอาจทดสอบได้ไม่ง่าย

 // hard to test $('button').on('click', () => { $.getJSON('/path/to/data') .then(data => { $('#my-list').html('results: ' + data.join(', ')); }); }); // testable; we can directly run fetchThings to see if it // makes an AJAX request without having to trigger DOM // events, and we can run showThings directly to see that it // displays data in the DOM without doing an AJAX request $('button').on('click', () => fetchThings(showThings)); function fetchThings(callback) { $.getJSON('/path/to/data').then(callback); } function showThings(data) { $('#my-list').html('results: ' + data.join(', ')); }

ใช้การโทรกลับหรือสัญญาด้วยรหัสอะซิงโครนัส

ในตัวอย่างโค้ดด้านบน ฟังก์ชัน fetchThings ที่ปรับโครงสร้างใหม่ของเราเรียกใช้คำขอ AJAX ซึ่งทำงานส่วนใหญ่แบบอะซิงโครนัส ซึ่งหมายความว่าเราไม่สามารถเรียกใช้ฟังก์ชันและทดสอบว่าได้ทำทุกอย่างที่เราคาดไว้ เพราะเราจะไม่ทราบว่าฟังก์ชันนี้ทำงานเสร็จเมื่อใด

วิธีที่พบบ่อยที่สุดในการแก้ปัญหานี้คือการส่งฟังก์ชันเรียกกลับเป็นพารามิเตอร์ไปยังฟังก์ชันที่ทำงานแบบอะซิงโครนัส ในการทดสอบหน่วยของคุณ คุณสามารถเรียกใช้การยืนยันของคุณในการโทรกลับที่คุณผ่าน

ภาพประกอบ: การใช้การเรียกกลับเป็นพารามิเตอร์ในการทดสอบหน่วย

อีกวิธีหนึ่งที่ใช้กันทั่วไปและเป็นที่นิยมมากขึ้นในการจัดระเบียบโค้ดแบบอะซิงโครนัสคือการใช้ Promise API โชคดีที่ $.ajax และฟังก์ชันอะซิงโครนัสอื่นๆ ของ jQuery ส่งคืนอ็อบเจ็กต์ Promise แล้ว ดังนั้นกรณีการใช้งานทั่วไปจำนวนมากจึงครอบคลุมอยู่แล้ว

 // hard to test; we don't know how long the AJAX request will run function fetchData() { $.ajax({ url: '/path/to/data' }); } // testable; we can pass a callback and run assertions inside it function fetchDataWithCallback(callback) { $.ajax({ url: '/path/to/data', success: callback, }); } // also testable; we can run assertions when the returned Promise resolves function fetchDataWithPromise() { return $.ajax({ url: '/path/to/data' }); }

หลีกเลี่ยงผลข้างเคียง

เขียนฟังก์ชันที่รับอาร์กิวเมนต์และส่งกลับค่าโดยอิงตามอาร์กิวเมนต์เหล่านั้นเท่านั้น เช่นเดียวกับการต่อยตัวเลขลงในสมการทางคณิตศาสตร์เพื่อให้ได้ผลลัพธ์ หากฟังก์ชันของคุณขึ้นอยู่กับสถานะภายนอกบางอย่าง (เช่น คุณสมบัติของอินสแตนซ์ของคลาสหรือเนื้อหาของไฟล์ เป็นต้น) และคุณต้องตั้งค่าสถานะนั้นก่อนทดสอบฟังก์ชัน คุณจะต้องตั้งค่าเพิ่มเติมในการทดสอบ คุณจะต้องเชื่อมั่นว่าโค้ดอื่นๆ ที่กำลังรันอยู่จะไม่เปลี่ยนแปลงสถานะเดียวกันนั้น

ภาพประกอบ: เอฟเฟกต์ Cascading ที่เกิดจากสถานะภายนอก

ในทำนองเดียวกัน ให้หลีกเลี่ยงการเขียนฟังก์ชันที่เปลี่ยนแปลงสถานะภายนอก (เช่น การเขียนไปยังไฟล์หรือการบันทึกค่าลงในฐานข้อมูล) ในขณะที่ทำงาน ซึ่งจะป้องกันผลข้างเคียงที่อาจส่งผลต่อความสามารถในการทดสอบโค้ดอื่นๆ ของคุณได้อย่างมั่นใจ โดยทั่วไป วิธีที่ดีที่สุดคือเก็บผลข้างเคียงไว้ใกล้กับขอบโค้ดของคุณมากที่สุด โดยให้มี "พื้นที่พื้นผิว" น้อยที่สุด ในกรณีของคลาสและอินสแตนซ์อ็อบเจ็กต์ ผลข้างเคียงของเมธอดของคลาสควรจำกัดอยู่ที่สถานะของอินสแตนซ์คลาสที่กำลังทดสอบ

 // hard to test; we have to set up a globalListOfCars object and set up a // DOM with a #list-of-models node to test this code function processCarData() { const models = globalListOfCars.map(car => car.model); $('#list-of-models').html(models.join(', ')); } // easy to test; we can pass an argument and test its return value, without // setting any global values on the window or checking the DOM the result function buildModelsString(cars) { const models = cars.map(car => car.model); return models.join(','); }

ใช้การฉีดพึ่งพา

รูปแบบทั่วไปอย่างหนึ่งสำหรับการลดการใช้สถานะภายนอกของฟังก์ชันคือการฉีดการพึ่งพา ซึ่งส่งผ่านความต้องการภายนอกของฟังก์ชันทั้งหมดเป็นพารามิเตอร์ของฟังก์ชัน

 // depends on an external state database connector instance; hard to test function updateRow(rowId, data) { myGlobalDatabaseConnector.update(rowId, data); } // takes a database connector instance in as an argument; easy to test! function updateRow(rowId, data, databaseConnector) { databaseConnector.update(rowId, data); }

ประโยชน์หลักของการใช้การพึ่งพาการฉีดคือคุณสามารถส่งผ่านวัตถุจำลองจากการทดสอบหน่วยของคุณที่ไม่ก่อให้เกิดผลข้างเคียงจริง (ในกรณีนี้คือการอัปเดตแถวฐานข้อมูล) และคุณสามารถยืนยันได้ว่าวัตถุจำลองของคุณถูกดำเนินการ ในทางที่คาดหวัง

ให้แต่ละหน้าที่มีวัตถุประสงค์เดียว

แบ่งฟังก์ชันแบบยาวที่ทำหลายอย่างออกเป็นคอลเลกชั่นของฟังก์ชันแบบวัตถุประสงค์เดียวแบบสั้น วิธีนี้ทำให้ง่ายต่อการทดสอบว่าแต่ละฟังก์ชันทำงานอย่างถูกต้อง แทนที่จะหวังว่าฟังก์ชันขนาดใหญ่จะทำทุกอย่างอย่างถูกต้องก่อนที่จะคืนค่ากลับ

ในการเขียนโปรแกรมเชิงฟังก์ชัน การรวมฟังก์ชันวัตถุประสงค์เดียวหลายๆ อย่างเข้าด้วยกันเรียกว่าการแต่ง Underscore.js ยังมีฟังก์ชัน _.compose ซึ่งรับรายการฟังก์ชันและเชื่อมโยงเข้าด้วยกัน โดยนำค่าที่ส่งคืนของแต่ละขั้นตอนและส่งไปยังฟังก์ชันถัดไปในบรรทัด

 // hard to test function createGreeting(name, location, age) { let greeting; if (location === 'Mexico') { greeting = '!Hola'; } else { greeting = 'Hello'; } greeting += ' ' + name.toUpperCase() + '! '; greeting += 'You are ' + age + ' years old.'; return greeting; } // easy to test function getBeginning(location) { if (location === 'Mexico') { return 'Hola'; } else { return 'Hello'; } } function getMiddle(name) { return ' ' + name.toUpperCase() + '! '; } function getEnd(age) { return 'You are ' + age + ' years old.'; } function createGreeting(name, location, age) { return getBeginning(location) + getMiddle(name) + getEnd(age); }

อย่าเปลี่ยนพารามิเตอร์

ใน JavaScript อาร์เรย์และอ็อบเจ็กต์จะถูกส่งผ่านโดยการอ้างอิงมากกว่าค่า และสามารถเปลี่ยนแปลงได้ ซึ่งหมายความว่าเมื่อคุณส่งผ่านอ็อบเจ็กต์หรืออาร์เรย์เป็นพารามิเตอร์ไปยังฟังก์ชัน ทั้งโค้ดและฟังก์ชันของคุณ คุณได้ส่งผ่านอ็อบเจ็กต์หรืออาร์เรย์เพื่อให้สามารถแก้ไขอินสแตนซ์เดียวกันของอาร์เรย์หรืออ็อบเจ็กต์ในหน่วยความจำได้ ซึ่งหมายความว่าหากคุณกำลังทดสอบโค้ดของคุณเอง คุณต้องวางใจว่าไม่มีฟังก์ชันใดที่การเรียกใช้โค้ดของคุณเปลี่ยนแปลงอ็อบเจ็กต์ของคุณ ทุกครั้งที่คุณเพิ่มตำแหน่งใหม่ในโค้ดของคุณที่เปลี่ยนแปลงอ็อบเจ็กต์เดิม การติดตามว่าอ็อบเจกต์นั้นควรมีลักษณะเป็นอย่างไร ยากขึ้นเรื่อยๆ ซึ่งทำให้การทดสอบยากขึ้น

ภาพประกอบ: พารามิเตอร์การกลายพันธุ์อาจทำให้เกิดปัญหาได้

ถ้าคุณมีฟังก์ชันที่รับอ็อบเจ็กต์หรืออาร์เรย์ ให้ฟังก์ชันนั้นทำงานบนอ็อบเจ็กต์หรืออาร์เรย์นั้นเสมือนว่าเป็นแบบอ่านอย่างเดียว สร้างวัตถุหรืออาร์เรย์ใหม่ในโค้ดและเพิ่มค่าตามความต้องการของคุณ หรือใช้ Underscore หรือ Lodash เพื่อโคลนวัตถุหรืออาร์เรย์ที่ส่งผ่านก่อนที่จะดำเนินการ ยิ่งไปกว่านั้น ให้ใช้เครื่องมือเช่น Immutable.js ที่สร้างโครงสร้างข้อมูลแบบอ่านอย่างเดียว

 // alters objects passed to it function upperCaseLocation(customerInfo) { customerInfo.location = customerInfo.location.toUpperCase(); return customerInfo; } // sends a new object back instead function upperCaseLocation(customerInfo) { return { name: customerInfo.name, location: customerInfo.location.toUpperCase(), age: customerInfo.age }; }

เขียนการทดสอบของคุณก่อนรหัสของคุณ

กระบวนการเขียนการทดสอบหน่วยก่อนโค้ดที่พวกเขากำลังทดสอบเรียกว่าการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ (TDD) นักพัฒนาซอฟต์แวร์จำนวนมากพบว่า TDD มีประโยชน์มาก

โดยการเขียนการทดสอบของคุณก่อน คุณจะถูกบังคับให้นึกถึง API ที่คุณเปิดเผยจากมุมมองของนักพัฒนาที่ใช้มัน นอกจากนี้ยังช่วยให้มั่นใจว่าคุณเขียนโค้ดเพียงพอเพื่อให้เป็นไปตามสัญญาที่การทดสอบของคุณบังคับใช้ แทนที่จะเขียนทับโซลูชันที่ซับซ้อนโดยไม่จำเป็น

ในทางปฏิบัติ TDD เป็นวินัยที่อาจเป็นเรื่องยากสำหรับการเปลี่ยนแปลงโค้ดทั้งหมดของคุณ แต่เมื่อดูเหมือนว่าจะคุ้มค่าที่จะลอง มันเป็นวิธีที่ยอดเยี่ยมในการรับประกันว่าคุณสามารถทดสอบโค้ดทั้งหมดได้

สรุป

เราทุกคนทราบดีว่ามีข้อผิดพลาดบางประการที่มักเกิดขึ้นได้ง่ายเมื่อเขียนและทดสอบแอป JavaScript ที่ซับซ้อน แต่หวังว่าด้วยเคล็ดลับเหล่านี้ และอย่าลืมรักษาโค้ดของเราให้เรียบง่ายและใช้งานได้ดีที่สุดเสมอ เราสามารถรักษาความครอบคลุมของการทดสอบให้สูงและความซับซ้อนของโค้ดโดยรวมต่ำ!

ที่เกี่ยวข้อง:
  • 10 ข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนา JavaScript สร้าง
  • Need for Speed: A Toptal JavaScript Coding Challenge ย้อนหลัง