• בלוג
  • איך נולד באג (או: על החשיבות של בדיקות כשמשנים קוד)

איך נולד באג (או: על החשיבות של בדיקות כשמשנים קוד)

28/12/2023

נתבונן בקוד הבא ב node.js:

import net from 'node:net';

const host = "localhost";
const port = 1234;

const socket = net.createConnection(port, host, () => {
  socket.write("hello\n");
  socket.end();
});

socket.on('error', () => {
  console.log(`Error connecting to ${host}:${port}`);
});

הקוד מנסה להתחבר לפורט 1234 על המכונה המקומית, לשלוח הודעה hello ואז לסגור את החיבור. אם יש לי מישהו שמקשיב על הפורט הכל יעבוד, ואם אף אחד לא מקשיב נגיע לאירוע error והודעת השגיאה תודפס על המסך. עד פה הכל טוב.

עכשיו נדמיין דרישה חדשה למערכת - יש לאפשר לקבוע את הפורט מבחוץ באמצעות משתנה סביבה. אם לא הוגדר ערך במשתנה הסביבה נשתמש עדיין ב 1234 בתור ברירת מחדל. הקוד המעודכן עשוי להיראות כך:

import net from 'node:net';

const host = "localhost";
const port = Number(process.env["PORT"]) || 1234;

const socket = net.createConnection(port, host, () => {
  socket.write("hello\n");
  socket.end();
});

socket.on('error', () => {
  console.log(`Error connecting to ${host}:${port}`);
});

נראה טוב? תחשבו שוב. ב JavaScript הפונקציה Number מחזירה מספר, אבל מספר יכול להיות מאוד גדול או מאוד קטן או אפילו כזה שמכיל נקודה עשרונית. בלי לשים לב איפשרנו למערכת לקבל מספרי פורט לא תקניים, מה שיגרום ל createConnection להיכשל לפני שמגיע לאירוע error. זה נראה ככה:

$ node demo.mjs
Error connecting to localhost:1234

$ PORT=8080 node demo.mjs
Error connecting to localhost:8080

$ PORT=abc node demo.mjs
Error connecting to localhost:1234

$ PORT=-1 node demo.mjs
node:internal/errors:496
    ErrorCaptureStackTrace(err);
    ^

RangeError [ERR_SOCKET_BAD_PORT]: Port should be >= 0 and < 65536. Received type number (-1).
    at new NodeError (node:internal/errors:405:5)
    at validatePort (node:internal/validators:409:11)
    at lookupAndConnect (node:net:1289:5)
    at Socket.connect (node:net:1246:5)
    at Object.connect (node:net:236:17)
    at file:///Users/ynonp/tmp/blog/nodebug/demo.mjs:6:20
    at ModuleJob.run (node:internal/modules/esm/module_job:217:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:308:24)
    at async loadESM (node:internal/process/esm_loader:42:7)
    at async handleMainPromise (node:internal/modules/run_main:66:12) {
  code: 'ERR_SOCKET_BAD_PORT'
}

בשלוש הדוגמאות הראשונות כשאין פורט או כשהפורט מכיל ערך שאינו מספר הכל בסדר ואנחנו נשארים עם ניסיון התחברות ואז כניסה לאירוע השגיאה. הדוגמה האחרונה יצרה מצב חדש שבורח ממסלולי הקוד שיצרנו וזורק שגיאה שלא טיפלנו בה.

הרבה פעמים אנחנו מדברים בבדיקות על Code Coverage בתור מדד שעוזר לנו להבין אם בדקנו כמו שצריך את המערכת. כיסוי קוד טוב אומר שכל הנתיבים נבדקו. אבל יותר נכון להסתכל על "כיסוי קלט", כלומר האם הבדיקות שלנו כיסו מספיק קלטים שונים שמייצגים את ההסתעפויות אליהן נגיע. דרך אחת להמציא קלטים טובים לבדיקה היא להיעזר ב Chat GPT. אלה האופציות שהוא נתן כשביקשתי ממנו ערכים אפשריים למשתנה PORT בתוכנית כדי לבדוק שהכל עובד:

# Test with default port
PORT=1234 node your_program.js

# Test with common ports
PORT=80 node your_program.js
PORT=443 node your_program.js

# Test with invalid port numbers
PORT=-1 node your_program.js
PORT=abc node your_program.js

# Test with a port already in use
# (Make sure another program is already using the specified port)
PORT=8080 node your_program.js

# Test with a custom port using environment variable
PORT=5678 node your_program.js