الاختبار 🧪
دليل كتابة وتشغيل اختبارات 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');
});