الاختبار 🧪

دليل كتابة وتشغيل اختبارات OpenClaw.

📋 نظرة عامة على الاختبارات

  • Vitest: إطار عمل الاختبار
  • اختبارات الوحدة: اختبار وظائف معزولة
  • اختبارات التكامل: اختبار مكونات متعددة
  • اختبارات E2E: سير عمل كامل end-to-end

الإعداد

تثبيت التبعيات

cd openclaw
pnpm install

تكوين بيئة الاختبار

# إنشاء .env.test
cp .env.example .env.test

# ملء متغيرات الاختبار
# استخدم مفاتيح API اختبار إن أمكن
ANTHROPIC_API_KEY=test-key
OPENAI_API_KEY=test-key

تشغيل الاختبارات

تشغيل جميع الاختبارات

# تشغيل جميع الاختبارات
pnpm test

# مع التغطية
pnpm test:coverage

# في وضع المراقبة
pnpm test:watch

تشغيل اختبارات محددة

# اختبار ملف واحد
pnpm test src/gateway/config.test.ts

# اختبار بنمط معين
pnpm test --grep "gateway"

# اختبار مجلد
pnpm test tests/unit/

كتابة الاختبارات

اختبار وحدة بسيط

// src/utils/format.test.ts
import { describe, it, expect } from 'vitest';
import { formatMessage } from './format';

describe('formatMessage', () => {
  it('should format basic message', () => {
    const result = formatMessage('Hello');
    expect(result).toBe('Hello');
  });

  it('should trim whitespace', () => {
    const result = formatMessage('  Hello  ');
    expect(result).toBe('Hello');
  });

  it('should handle empty string', () => {
    const result = formatMessage('');
    expect(result).toBe('');
  });
});

اختبار مع Mocks

// src/gateway/api.test.ts
import { describe, it, expect, vi } from 'vitest';
import { callAPI } from './api';

// Mock fetch
global.fetch = vi.fn();

describe('callAPI', () => {
  it('should make API call', async () => {
    const mockResponse = { data: 'test' };
    (fetch as any).mockResolvedValueOnce({
      ok: true,
      json: async () => mockResponse,
    });

    const result = await callAPI('/test');
    
    expect(fetch).toHaveBeenCalledWith('/test');
    expect(result).toEqual(mockResponse);
  });

  it('should handle errors', async () => {
    (fetch as any).mockRejectedValueOnce(new Error('Network error'));

    await expect(callAPI('/test')).rejects.toThrow('Network error');
  });
});

اختبار Async

// src/agent/session.test.ts
import { describe, it, expect } from 'vitest';
import { createSession } from './session';

describe('createSession', () => {
  it('should create new session', async () => {
    const session = await createSession({
      agentId: 'test',
      userId: 'user123',
    });

    expect(session).toHaveProperty('id');
    expect(session.agentId).toBe('test');
    expect(session.userId).toBe('user123');
  });
});

أنماط الاختبار

اختبارات الوحدة

اختبر وظائف ووحدات معزولة:

  • دوال الأدوات
  • معالجة التكوين
  • تنسيق الرسائل
  • عمليات التحقق من الصحة

اختبارات التكامل

اختبر تفاعل المكونات:

  • Gateway + Agent
  • القنوات + معالجة الرسائل
  • الجلسات + التخزين
  • نماذج + موفرين
// tests/integration/gateway.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { startGateway, stopGateway } from './helpers';

describe('Gateway Integration', () => {
  beforeAll(async () => {
    await startGateway();
  });

  afterAll(async () => {
    await stopGateway();
  });

  it('should handle agent request', async () => {
    const response = await fetch('http://localhost:18789/agent', {
      method: 'POST',
      body: JSON.stringify({ message: 'Hello' }),
    });

    expect(response.ok).toBe(true);
    const data = await response.json();
    expect(data).toHaveProperty('response');
  });
});

اختبارات E2E

اختبر سير عمل كامل من البداية للنهاية:

// tests/e2e/whatsapp.test.ts
import { describe, it, expect } from 'vitest';
import { sendWhatsAppMessage, waitForResponse } from './helpers';

describe('WhatsApp E2E', () => {
  it('should send and receive message', async () => {
    // إرسال رسالة
    await sendWhatsAppMessage('Hello, bot!');

    // انتظار الرد
    const response = await waitForResponse(5000);

    expect(response).toContain('Hello');
  });
});

أفضل الممارسات

✅ افعل

  • اكتب اختبارات واضحة ووصفية
  • اختبر حالات Edge
  • استخدم أسماء اختبارات وصفية
  • نظّف بعد الاختبارات (cleanup)
  • احتفظ بالاختبارات مستقلة
  • استخدم fixtures للبيانات

❌ لا تفعل

  • لا تعتمد على ترتيب الاختبارات
  • لا تستخدم مفاتيح API حقيقية في الاختبارات
  • لا تختبر implementation details
  • لا تترك اختبارات معطّلة (.skip)

تغطية الاختبار

توليد تقرير التغطية

# تشغيل مع التغطية
pnpm test:coverage

# عرض تقرير HTML
open coverage/index.html  # macOS
xdg-open coverage/index.html  # Linux
start coverage/index.html  # Windows

أهداف التغطية

  • الوظائف: ≥ 80%
  • الفروع: ≥ 70%
  • الأسطر: ≥ 80%

CI/CD Integration

GitHub Actions

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v3
        with:
          node-version: 22
          cache: 'pnpm'
      
      - run: pnpm install
      - run: pnpm test:coverage
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

مساعدات الاختبار

إنشاء بيانات اختبار

// tests/helpers/fixtures.ts
export function createTestAgent(overrides = {}) {
  return {
    id: 'test-agent',
    name: 'Test Agent',
    model: 'claude-3-opus',
    ...overrides,
  };
}

export function createTestSession(overrides = {}) {
  return {
    id: 'test-session',
    agentId: 'test-agent',
    userId: 'test-user',
    createdAt: new Date(),
    ...overrides,
  };
}

مساعدات Mock

// tests/helpers/mocks.ts
import { vi } from 'vitest';

export function mockAnthropicAPI() {
  return vi.fn().mockResolvedValue({
    content: 'Test response',
    usage: { tokens: 10 },
  });
}

export function mockWhatsAppChannel() {
  return {
    send: vi.fn(),
    on: vi.fn(),
    disconnect: vi.fn(),
  };
}

💡 نصيحة: اختبار الأخطاء

اختبر دائماً حالات الخطأ بالإضافة إلى الحالات السعيدة:

it('should handle API timeout', async () => {
  await expect(
    callAPI('/slow', { timeout: 1 })
  ).rejects.toThrow('Timeout');
});