כלי המקצוע

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

כפי שצוין בקטע הקודם, הפרימיטיבים האלה כמעט תמיד כוללים דרך להגדיר בדיקות עצמאיות (שנקראות מקרי בדיקה) ולספק טענות נכונות (assertions). טענות נכוֹנוּת (assertions) הן דרך לשלב בדיקת תוצאה והצגת שגיאה אם משהו השתבש, והיא יכולה להיחשב כפרימיטיבית הבסיסית של כל פרימיטיב הבדיקה.

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

לדוגמה:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

הדוגמה הזו יוצרת קבוצה של בדיקות (שלפעמים נקראת Suite) שנקראת 'בדיקות מתמטיות', ומגדירה שלושה מקרי בדיקה עצמאיים שכל אחד מהם מריץ טענות נכונות (assertions). בדרך כלל אפשר לטפל במקרי הבדיקה האלה או להריץ אותם בנפרד, למשל באמצעות סימון סינון בשירותי הבדיקה.

מסייעים בטענת נכוֹנוּת (assertion) כפרימיטיבים

רוב ה-frameworks לבדיקה, כולל Vitest, כוללות אוסף של רכיבי עזר לטענת נכוֹנוּת (assertion) באובייקט assert, שמאפשרים לבדוק במהירות ערכי החזרה או מצבים אחרים מול expectation מסוימות. ציפייה זו היא לעיתים קרובות ערכים שידועים כטובים. לפי הדוגמה הקודמת, אנחנו יודעים שמספר פיבונאצ'י ה-13 צריך להיות 233, אז אנחנו יכולים לאמת זאת ישירות באמצעות assert.equal.

יכול להיות שאתם גם מצפים שערך מסוים מופיע בצורה מסוימת, גדול מערך אחר או שיש לו תכונה אחרת. הקורס הזה לא יכסה את כל מגוון המסייעים האפשריים לטענת נכוֹנוּת (assertion), אבל המסגרות לבדיקת מסגרות תמיד מספקות לפחות את הבדיקות הבסיסיות הבאות:

  • בדיקת 'truthy', המתוארת לעיתים קרובות כבדיקה מסוג 'OK', בודקת שתנאי מסוים מתקיים, ומתאימה לאופן שבו כותבים if שבודק אם משהו תקין או תקין. בדרך כלל הנתונים מופיעים כ-assert(...) או assert.ok(...), ויש להם ערך יחיד בתוספת הערה אופציונלית.

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

    • מערכת JavaScript מבחינה בין שחרור (==) לבין שוויון מחמיר (===). רוב ספריות הבדיקה מספקות את השיטות assert.equal ו-assert.strictEqual, בהתאמה.
  • בדיקות שוויון עמוקות שמרחיבות את בדיקות השוויון כך שהן כוללות בדיקת התוכן של אובייקטים, מערכים וסוגי נתונים מורכבים יותר, וגם את הלוגיקה הפנימית של מעבר אובייקטים כדי להשוות ביניהם. הפרטים האלה חשובים כי ל-JavaScript אין דרך מובנית להשוות בין התוכן של שני אובייקטים או מערכים. למשל, [1,2,3] == [1,2,3] תמיד תהיה FALSE. לרוב, מסגרות בדיקה כוללות רכיבי עזר מסוג deepEqual או deepStrictEqual.

עזרים של טענת נכוֹנוּת (assertion) שמשווים שני ערכים (במקום בדיקה 'אמת' בלבד) בדרך כלל משתמשים בשניים או שלושה ארגומנטים:

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

בנוסף, לעיתים קרובות מאוד משלבים טענות נכוֹנוּת (assertions) כדי ליצור מגוון בדיקות, כי רק במקרים נדירים שבהם אפשר לאמת בצורה נכונה את מצב המערכת בעצמו. לדוגמה:

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

ב-Vitest משתמשים בתוך ספריית Chai לטענת נכוֹנוּת (assertion) באופן פנימי, וכדאי לבדוק בהפניות שלה כדי לבדוק אילו טענות נכונות (assertions) ותכונות מסייעות יכולות להתאים לקוד שלכם.

טענות שוטפות ו-BDD

חלק מהמפתחים מעדיפים סגנון של טענת נכוֹנוּת (assertion) שאפשר לקרוא לו פיתוח מבוסס-התנהגות (BDD) או טענות נכונות (assertions) בסגנון שוטף. האמצעים האלה נקראים גם כלי עזר מסוג "ציפיות", כי נקודת הכניסה לבדיקת ציפיות היא שיטה expect().

יש לצפות שהעוזרים יתנהגו כמו טענות נכוֹנוּת (assertion) שנכתבו כקריאות פשוטות לשיטה כמו assert.ok או assert.strictDeepEquals, אבל לחלק מהמפתחים קל יותר לקרוא אותן. טענת נכוֹנוּת של BDD יכולה להיראות כך:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

הסגנון של טענות הנכוֹנוּת (assertions) פועל בגלל שיטה שנקראת 'שרשור שיטות', שבה אפשר לשרשר באופן רציף את האובייקט שמוחזר על ידי expect בקריאות נוספות ל-method. לחלקים מסוימים בשיחה, כולל to.be ו-that.does בדוגמה הקודמת, אין פונקציה והפעולה כוללת רק כדי שיהיה קל יותר לקרוא את הקריאה, וליצור תגובה אוטומטית במקרה שהבדיקה תיכשל. (חשוב לשים לב: expect בדרך כלל לא תומך בתגובה אופציונלית כי השרשור צריך לתאר את הכשל בצורה ברורה).

מסגרות בדיקה רבות תומכות גם ב-Fluent/BDD וגם בטענות נכונות (assertions) רגילות. לדוגמה, Vitest, מייצאת את שתי הגישות של Chat, ויש לה גישה תמציתית קצת יותר לגבי BDD. מצד שני, Jest כולל רק שיטת הציפיות כברירת מחדל.

קיבוץ בדיקות בקבצים שונים

בכתיבת בדיקות, אנחנו כבר נוטים לספק קבוצות משתמעות – במקום שכל הבדיקות בקובץ אחד, מקובל לכתוב בדיקות בקבצים מרובים. למעשה, רצים של בדיקות בדרך כלל יודעים שהקובץ מיועד לבדיקה רק בגלל מסנן מוגדר מראש או ביטוי רגולרי. לדוגמה, Vitest כולל את כל הקבצים בפרויקט שמסתיימים בסיומת כמו " .test.jsx" או " .spec.ts"(".test" ו-".spec" בתוספת מספר תוספים חוקיים).

בדיקות הרכיבים בדרך כלל ממוקמות בקובץ עמית לרכיב שנבדק, כמו במבנה הספריות הבא:

רשימה של קבצים בספרייה, כולל UserList.tsx ו-UserList.test.tsx.
קובץ רכיב וקובץ בדיקה קשור.

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

קיבוץ בדיקות בתוך קבצים

כמו בדוגמאות הקודמות, מקובל לבצע בדיקות בקריאה ל-suite() שמקבצת בדיקות שהוגדרו באמצעות test(). הסוויטות לא בדרך כלל בודקות בעצמן, אבל הן עוזרות ליצור מבנה על ידי קיבוץ בדיקות או יעדים קשורים על ידי קריאה לשיטה שהועברה. לגבי test(), השיטה שהועברה מתארת את פעולות הבדיקה עצמה.

כמו בטענות נכונות (assertions), יש שווה ערך סטנדרטי למדי ב-Fluent/BDD למבחני קיבוץ. הקוד הבא כולל השוואה בין כמה דוגמאות אופייניות:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

ברוב המסגרות, suite ו-describe מתנהגים באופן דומה, וכך גם test ו-it, בניגוד להבדלים הגדולים יותר בין השימוש ב-expect וב-assert לכתיבת טענות נכוֹנוּת (assertions).

לכלים אחרים יש גישות שונות מאוד לסידור חבילות ובדיקות. לדוגמה, קובץ הבדיקה המובנה של Node.js תומך בקינון קריאות ל-test() כדי ליצור באופן מרומז היררכיית בדיקות. עם זאת, Vitest מאפשר קינון מהסוג הזה רק באמצעות suite(), ולא יריץ test() שמוגדר בתוך test() אחר.

בדיוק כמו בטענות נכוֹנוּת (assertion), חשוב לזכור שהשילוב המדויק של שיטות הקיבוץ שסטאק התוכנות מספק לא חשוב כל כך. קורס זה יעסוק בהם מופשט, אבל תצטרכו להבין איך הם חלים על הכלים שבחרתם.

שיטות במחזור החיים

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

לכל 'test() ' או 'it() ' פעם אחת לסוויטה
לפני הפעלות בדיקה 'beforeEvery() ' 'beforeAll() '
אחרי הפעלות בדיקה 'afterBefore() ' 'afterAll() '

לדוגמה, יכול להיות שתרצו לאכלס מראש מסד נתונים של משתמשים וירטואלי לפני כל בדיקה ולמחוק אותו אחריה:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

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

עצה כללית

ריכזנו כאן כמה טיפים שכדאי לזכור כשחושבים על הפרימיטיבים האלה.

פרימיטיבים הם מנחים

זכרו שהכלים והפרימיטיבים שמופיעים כאן, ובדפים הבאים, לא יתאמו במדויק ל-Vitest, ל-Jest, ל-Mocha, ל-Web Test Runner או לכל מסגרת ספציפית אחרת. השתמשנו ב-Vitest כמדריך כללי, אבל הקפידו למפות אותם למסגרת שבחרתם.

משלבים ומתאימים טענות נכונות לפי הצורך

בדיקות הן קוד בסיסי שיכול לגרום לשגיאות. כל רץ יספק ערך ראשוני, שסביר להניח test(), כדי לתאר מקרי בדיקה ייחודיים.

אבל אם המסלול הזה מספק גם עזרים ל-assert(), ל-expect() ולטענות נכונות (assertion), זכרו שהחלק הזה עוסק יותר בנוחות, ותוכלו לדלג עליו במקרה הצורך. אפשר להריץ כל קוד שעלול לגרום לשגיאה, כולל ספריות אחרות של טענת נכוֹנוּת (assertion) או הצהרת if מיושנת.

הגדרת סביבת פיתוח משולבת (IDE) יכולה להציל חיים

כאשר בסביבת הפיתוח המשולבת (IDE), כמו VSCode, יש גישה להשלמה אוטומטית ולמסמכי תיעוד בכלי הבדיקה שבחרתם, כדי שתוכלו להיות פרודוקטיביים יותר. לדוגמה, יש יותר מ-100 שיטות ל-assert בספריית טענות נכוֹנוּת (assertion) של Chat, ואפשרות לשמור תיעוד של השיטה הנכונה בתוך השורה יכולה להיות נוחה.

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

// some.test.js
test('using test as a global', () => { … });

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

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });