בפוסט זה אשתף אתכם בניסיון שלי בכתיבת בדיקות יחידה (unit tests) לקוד בשפת C באמצעות הכלי C++Test של Parasoft. טיפים אלה אינם כתובים בשום מקום בתיעוד שמספקת Parasoft, ולקח לי זמן לפענח אותם בעצמי. בכך שאני משתף אתכם במידע זה, אני מקווה שזה יחסוך לכם הרבה זמן.

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

תנו ל-Parasoft ליצור עבורכם את בדיקות היחידה וה-stubs

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

  • אימות התוצאות של כל בדיקה
  • שיפור הכיסוי (coverage) של הבדיקות שלכם
  • תיקון בעיות בבדיקות כגון חריגות זיכרון (memory exceptions)

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

שימו לב שמשתנה הקלט לא בהכרח יוגדר באופן שישפר את הכיסוי של הקוד, ויש לכם מידה מסוימת של שליטה על האופן שבו הם מוגדרים תחת “Test Configurations” של תצורות “Generate Unit Tests” או “Generate Test Suites”.

Parasoft C++TEST: Test Configurations-->Generate Unit Tests-->Generation-->Test Case Example for defining inputs for unit tests

שליחת מערכים לפונקציות – דרך בטוחה ויעילה יותר לעבודה מהירה יותר עם Parasoft C++Test

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

שקלו פונקציה המקבלת מערך כקלט ומקצה את הערך 0 ל-10 המיקומים הראשונים:

void FuncArrayTen(uint8_t* ucArr)
{
    for(uint8_t ucIndex ; ucIndex < 10u ; ucIndex++ )
        ucArr[ucIndex] = 0u;
}

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

void FuncArrayTen (uint8_t *ucArr, uint8_t ucSize)
{
    if( ucSize < 10u )
        return;

    for(uint8_t ucIndex=0 ; ucIndex < 10u ; ucIndex++ )
        ucArr[ucIndex] = 0u;
}

אך ישנן מספר בעיות עם פתרון פופולרי זה:

  1. אם תיתנו ל-parasoft ליצור בדיקת יחידה עבור FuncArrayTen אוטומטית, אז הוא לא “ידע” שהקלט הראשון צריך להיות מערך. הוא ייצור מצביע (pointer) למשתנה וישלח אותו לפונקציה. הוא לא “ידע” ש-ucSize צריך לשקף את גודל המערך, ויכול להקצות לו ערך הגדול מ-1 אך קטן מ-10, מה שיגרום לחריגה עקב גישה למרחב זיכרון לא מוגדר.
  2. ייתכן גם שיהיה לכם באג בקוד שלכם המגדיר את המצביע הנשלח למערך כמצביע למערך בגודל קטן מ-10, מה שיגרום להתנהגות לא מוגדרת בקוד שלכם.
  3. כשתכתבו קוד הקורא לפונקציה זו, ייתכן שתשכחו מה התכוונתם שגודל המערך יהיה כשיצרתם במקור את הפונקציה, מה שעלול להוביל לבלבול ולשגיאה אפשרית בהמשך. נכון, אתם יכולים להשתמש בהערות נאותות בקוד כדי לתעד את הפונקציה והקלטים שלה, אך זה גם דורש תחזוקה מתמדת אם יהיו שינויים עתידיים בפונקציה.

יש פתרון פשוט מאוד המכסה את כל הבעיות הללו. ניתן להגדיר את סוג הקלט שהפונקציה מקבלת כמשתנה מסוג מצביע למערך בגודל מוגדר מראש (במקרה זה 10) באמצעות התחביר הבא: *uint8_t (ucArr)[10]

אל תבלבלו את התחביר הנ”ל עם התחביר הזה: uint8_t * ucArr[10] שהוא שווה ערך לכתיבת *uint8_t *ucArr.

גם התחביר הבא לא יועיל לכם: uint8_t ucArr[10] מכיוון שהוא שווה ערך לכתיבת *uint8_t ucArr. עם זאת, ראוי לציין שבכתיבה בדרך זו, C++TEST ישלח לא רק מצביע למשתנה אלא ישלח בפועל מערכים – אך בגדלים משתנים ללא התחשבות בערך שתשימו בסוגריים. אז זה עדיין לא אידיאלי מכיוון שזה עלול להוביל לחריגות מרובות.

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

void FuncArrayTen (uint8_t (*ucArr)[10])
{
    for(uint8_t ucIndex=0 ; ucIndex < 10u ; ucIndex++ )
        ucArr[ucIndex] = 0u;
}

Uint8_t ucArray1[10];
Uint8_t ucArray2[15];

FuncArrayTen(&ucArray1);

שימו לב שאם הייתם קוראים לפונקציה כך: FuncArrayTen(ucArray1); או FuncArrayTen(&ucArray2) אז הייתם מקבלים אזהרה מהמהדר. כעת אתם מוגנים מטעויות שלכם עצמכם, והמהדר הופך למבקר (reviewer) שלכם!

  • בנוסף, Parasoft C++Test ייצור אוטומטית בדיקות יחידה עם קלטים כמערכים בגודל 10 במקום רק מצביע למשתנה בודד. אם בסופו של דבר אתם כותבים הרבה קוד, אז שימוש בתחביר בטוח יותר זה יחסוך לכם כמות עצומה של זמן עם C++Test של Parasoft מכיוון שלא תידרשו לתקן כל כך הרבה בדיקות.
  • שימו לב שגם כבר לא נדרש לבדוק את גודל המערך מכיוון שהוא כבר מאומת ברמת המהדר.

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

uint8_t CRCfunc(uint8_t (*ucArr)[20], uint8_t ucSize)
{
    if(ucSize > 20)
        return 0;
    for(uint8_t ucIndex=0 ; ucIndex < ucSize; ucIndex++ )
        // write logic here
}

החיסרון היחיד בעשייה בדרך זו הוא שקלט המערך של ucSize הנוצר אוטומטית על ידי C++TEST לא יסונכרן לגודל המערך הנשלח ל-ucArr (שהוא תמיד בגודל 20 בדוגמה לעיל). אך מכיוון שיש לנו את ההגנה הבודקת את ucSize בתוך הפונקציה, לא יהיו חריגות.

קריאה לפונקציות סטטיות ושימוש במשתנים גלובליים סטטיים בתוך הקובץ של הפונקציה הנבדקת

  1. כאשר Parasoft יוצר בדיקת יחידה עבור פונקציה בקוד שלכם הנמצאת ב-File.c, אז ייתכן שיֵראה ברור שמתוך פונקציית בדיקת היחידה אינכם יכולים לקרוא לפונקציה סטטית באותו קובץ.
  2. גם ייראה ברור שבתוך פונקציית בדיקת היחידה אינכם יכולים להשתמש במשתנה גלובלי סטטי כלשהו (משתנה שאינו מוגדר בתוך תחום פונקציה) שהוגדר ב-File.c.

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

#include File.c

(אבל אל תעשו זאת! אנא המשיכו לקרוא).

עם זאת, אם אי פעם ניסיתם לעשות דבר כזה, הייתם מגלים שזה בדרך כלל גורם לבעיה עצומה של פתרון תלויות (dependencies) במהלך הקישור (linking) והקומפילציה.

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

מדוע זה עובד? אני מניח שכחלק מהאוטומציה של יצירת בדיקות יחידה הם אכן כללו את File.c וגם פתרו את התלויות (שידנית עלול לקחת שעות אם לא ימים).

אז זה עוצמתי מאוד וזה יכול לעזור הרבה.