לדלג לתוכן

10.6 Vitest תרגול

תרגול - קונספט בדיקות ו-Vitest - Testing Concepts and Vitest

תרגיל 1 - בדיקות לפונקציות מחרוזת

כתבו את הפונקציות הבאות ובדיקות מלאות לכל אחת:

  1. capitalize(str) - הופכת את האות הראשונה לגדולה
  2. slugify(str) - ממירה מחרוזת ל-URL slug (אותיות קטנות, מקפים במקום רווחים, ללא תווים מיוחדים)
  3. truncate(str, maxLength) - מקצרת מחרוזת ומוסיפה "..." אם היא ארוכה מ-maxLength
  4. countWords(str) - סופרת מילים במחרוזת

כתבו לפחות 4 בדיקות לכל פונקציה, כולל מקרי קצה (מחרוזת ריקה, null, תווים מיוחדים).


תרגיל 2 - בדיקות לפונקציות מערך

כתבו את הפונקציות הבאות ובדיקות:

// 1. הסרת כפילויות
function unique<T>(arr: T[]): T[];

// 2. קיבוץ לפי מפתח
function groupBy<T>(arr: T[], key: keyof T): Record<string, T[]>;

// 3. מיון לפי כמה שדות
function sortBy<T>(arr: T[], ...keys: (keyof T)[]): T[];

// 4. שטוח מערך מקונן
function flatten<T>(arr: (T | T[])[]): T[];

// 5. חלוקה לקבוצות (chunks)
function chunk<T>(arr: T[], size: number): T[][];

כתבו את הפונקציות ולכל אחת לפחות 3 בדיקות.


תרגיל 3 - בדיקות TDD לעגלת קניות

השתמשו בגישת TDD לבניית מודול עגלת קניות. כתבו את הבדיקות קודם, ואז את הקוד.

הממשק:

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface Cart {
  items: CartItem[];
  addItem(item: Omit<CartItem, 'quantity'>, quantity?: number): void;
  removeItem(id: string): void;
  updateQuantity(id: string, quantity: number): void;
  getTotal(): number;
  getItemCount(): number;
  clear(): void;
  getItem(id: string): CartItem | undefined;
}

כללי עסקיים:
- הוספת מוצר שכבר קיים מעדכנת את הכמות
- כמות לא יכולה להיות שלילית
- הסרת מוצר שלא קיים לא גורמת לשגיאה
- סה"כ = סכום (מחיר * כמות) לכל פריט


תרגיל 4 - בדיקות לקוד אסינכרוני

נתונות הפונקציות הבאות. כתבו בדיקות מלאות:

// utils/api.ts
export async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`User ${id} not found`);
  }
  return response.json();
}

export async function fetchWithRetry(
  url: string,
  retries: number = 3,
  delay: number = 1000
): Promise<Response> {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (response.ok) return response;
    } catch (error) {
      if (i === retries - 1) throw error;
    }
    await new Promise(resolve => setTimeout(resolve, delay));
  }
  throw new Error(`Failed after ${retries} retries`);
}

export function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): T {
  let timeoutId: ReturnType<typeof setTimeout>;
  return ((...args: any[]) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  }) as T;
}

בדקו:
- fetchUser: תגובה מוצלחת, שגיאה 404, שגיאת רשת
- fetchWithRetry: הצלחה בניסיון ראשון, הצלחה אחרי retry, כישלון אחרי כל הניסיונות
- debounce: קריאה בודדת, קריאות מרובות (רק האחרונה רצה), delay נכון


תרגיל 5 - בדיקות לפונקציית Validator

כתבו validator מלא לטופס הרשמה ובדיקות מקיפות:

interface RegistrationForm {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
  age: number;
  agreeToTerms: boolean;
}

interface ValidationResult {
  valid: boolean;
  errors: Record<string, string>;
}

function validateRegistration(form: RegistrationForm): ValidationResult;

כללי validation:
- username: 3-20 תווים, רק אותיות, מספרים וקו תחתון
- email: פורמט אימייל תקין
- password: לפחות 8 תווים, אות גדולה, אות קטנה, מספר
- confirmPassword: חייב להיות זהה ל-password
- age: 13-120
- agreeToTerms: חייב להיות true


תרגיל 6 - בדיקות עם Setup/Teardown

כתבו בדיקות למחלקת LocalStorage wrapper:

class Storage {
  private prefix: string;

  constructor(prefix: string = 'app') {
    this.prefix = prefix;
  }

  set(key: string, value: any): void {
    localStorage.setItem(`${this.prefix}:${key}`, JSON.stringify(value));
  }

  get<T>(key: string, defaultValue?: T): T | undefined {
    const item = localStorage.getItem(`${this.prefix}:${key}`);
    if (item === null) return defaultValue;
    try {
      return JSON.parse(item);
    } catch {
      return defaultValue;
    }
  }

  remove(key: string): void {
    localStorage.removeItem(`${this.prefix}:${key}`);
  }

  clear(): void {
    const keys = Object.keys(localStorage);
    keys.forEach(key => {
      if (key.startsWith(`${this.prefix}:`)) {
        localStorage.removeItem(key);
      }
    });
  }

  has(key: string): boolean {
    return localStorage.getItem(`${this.prefix}:${key}`) !== null;
  }
}

השתמשו ב-beforeEach ו-afterEach לניקוי ה-localStorage בין בדיקות.


שאלות

  1. מה ההבדל בין בדיקת יחידה לבדיקת אינטגרציה? תנו דוגמה קונקרטית לכל אחת.
  2. הסבירו את תהליך TDD בשלושה שלבים. מה היתרון של לכתוב את הבדיקה קודם?
  3. מה ההבדל בין toBe ל-toEqual? מתי כל אחד מתאים?
  4. למה חשוב להשתמש ב-fake timers בבדיקות? מה הייתה הבעיה בלעדיהם?
  5. מה ההבדל בין beforeEach ל-beforeAll? מתי משתמשים בכל אחד?