{
  "title": "سیاست تست",
  "slug": "team/platform/standards/testing-policy",
  "url": "/docs/team/platform/standards/testing-policy",
  "frontmatter": {
    "layout": "doc",
    "title": "سیاست تست",
    "description": "تست واحد و یکپارچه، CI، پوشش، قوانین و ساختار پوشه تست",
    "version": "1.0.0",
    "status": "PRIVATE",
    "author": "xoxxel",
    "owner": "xoxxel",
    "created_at": "2026-06-07",
    "updated_at": "2026-06-07",
    "tags": "",
    "reviewers": ""
  },
  "sections": [
    {
      "level": 1,
      "heading": "سیاست تست",
      "content": "**Testing Policy**\n\nنسخه 1.0 | الزامی برای همه سرویس‌ها\n\n---"
    },
    {
      "level": 2,
      "heading": "1. اصل اساسی",
      "content": "**هر سرویس قبل از merge به main باید تست داشته باشد.**\n\n---"
    },
    {
      "level": 2,
      "heading": "2. انواع تست",
      "content": "| نوع | هدف پوشش | چه چیزی را تست می‌کند | وابستگی |\n|---|---|---|---|\n| واحد (Unit) | منطق بحرانی کسب‌وکار | توابع خالص، ماشین حالت، محاسبات | ❌ بدون I/O |\n| یکپارچه (Integration) | مسیر خوشحال + مسیرهای خطای اصلی | دیتابیس واقعی، NATS واقعی | ✅ دیتابیس + NATS واقعی |\n\n---"
    },
    {
      "level": 2,
      "heading": "3. تست واحد (Unit Test)",
      "content": ""
    },
    {
      "level": 3,
      "heading": "چه چیزی را تست کنیم",
      "content": "- منطق خالص کسب‌وکار\n- ماشین حالت (State Machine)\n- محاسبات و تبدیل‌ها\n- اعتبارسنجی\n- توابع کمکی (Helpers/Utils)"
    },
    {
      "level": 3,
      "heading": "مثال",
      "content": "```typescript\n// unit/order-status.test.ts\ndescribe('OrderStateMachine', () => {\n  it('should transition from pending to paid', () => {\n    const result = transition('pending', 'pay');\n    expect(result).toBe('paid');\n  });\n\n  it('should throw on invalid transition', () => {\n    expect(() => transition('paid', 'pay')).toThrow('Invalid transition');\n  });\n});\n```"
    },
    {
      "level": 3,
      "heading": "قوانین",
      "content": "| قانون | توضیح |\n|---|---|\n| **بدون I/O** | هیچ تماسی با دیتابیس، فایل سیستم یا شبکه |\n| **بدون Side Effect** | تست‌ها pure — بدون تغییر وضعیت خارجی |\n| **سریع** | هر تست واحد زیر ۱۰۰ms |\n| **ایزوله** | هر تست مستقل — ترتیب اجرا不重要 |\n\n---"
    },
    {
      "level": 2,
      "heading": "4. تست یکپارچه (Integration Test)",
      "content": ""
    },
    {
      "level": 3,
      "heading": "چه چیزی را تست کنیم",
      "content": "- مسیر خوشحال (Happy Path) کامل\n- مسیرهای خطای اصلی\n- تعامل با دیتابیس واقعی\n- انتشار و مصرف رویدادهای NATS"
    },
    {
      "level": 3,
      "heading": "مثال",
      "content": "```typescript\n// integration/order-create.test.ts\ndescribe('Create Order - Integration', () => {\n  it('should create order and publish event', async () => {\n    const order = await orderService.create({\n      sellerId: seller.id,\n      productId: product.id,\n      quantity: 1\n    });\n\n    expect(order.status).toBe('pending');\n    expect(order.id).toBeDefined();\n\n    // Verify event published\n    const event = await natsClient.waitForEvent('nons.order.created');\n    expect(event.payload.orderId).toBe(order.id);\n  });\n\n  it('should fail when seller has insufficient balance', async () => {\n    await expect(\n      orderService.create({\n        sellerId: poorSeller.id,\n        productId: expensiveProduct.id,\n        quantity: 100\n      })\n    ).rejects.toThrow('INSUFFICIENT_BALANCE');\n  });\n});\n```"
    },
    {
      "level": 3,
      "heading": "قوانین",
      "content": "| قانون | توضیح |\n|---|---|\n| **دیتابیس ایزوله** | هر تست یا suite دیتابیس تمیز دارد |\n| **بدون اشتراک وضعیت** | هرگز وضعیت بین تست‌ها به اشتراک گذاشته نشود |\n| **بدون Mock دیتابیس** | در تست یکپارچه از دیتابیس واقعی استفاده کنید — بدون mock |\n| **NATS واقعی** | از NATS واقعی یا embedded استفاده کنید |\n| **Cleanup** | پس از تست، داده‌ها پاک شوند |\n\n---"
    },
    {
      "level": 2,
      "heading": "5. ساختار پوشه تست",
      "content": "```\ntests/\n├── unit/\n│   ├── order-status.test.ts\n│   ├── payment-calc.test.ts\n│   └── helpers.test.ts\n└── integration/\n    ├── order-create.test.ts\n    ├── order-payment.test.ts\n    └── dispute-resolution.test.ts\n```\n\n---"
    },
    {
      "level": 2,
      "heading": "6. CI Pipeline",
      "content": "```mermaid\nflowchart LR\n    A[Push/PR] --> B[unit tests]\n    B --> C{همه قبول؟}\n    C -- بله --> D[integration tests]\n    C -- خیر --> E[رد PR]\n    D --> F{همه قبول؟}\n    F -- بله --> G[✅ قبول]\n    F -- خیر --> E\n```\n\n| مرحله | توضیح |\n|---|---|\n| **Push به هر برنچ** | اجرای تست‌های واحد |\n| **Push به برنچ feature** | اجرای تست‌های واحد + یکپارچه |\n| **Pull Request** | اجرای همه تست‌ها |\n| **Merge به main** | اجرای همه تست‌ها |\n| **شکست تست = مسدود شدن merge** | در صورت شکست، merge مجاز نیست |\n\n---"
    },
    {
      "level": 2,
      "heading": "7. پوشش تست (Test Coverage)",
      "content": "| نوع | حداقل پوشش | هدف ایده‌آل |\n|---|---|---|\n| Unit — منطق بحرانی | ۱۰۰٪ | ۱۰۰٪ |\n| Unit — کل سرویس | ۷۰٪ | ۸۰٪ |\n| Integration — مسیر خوشحال | ۱۰۰٪ مسیرهای اصلی | همه endpoints |\n| Integration — خطاها | خطاهای اصلی | همه سناریوهای خطا |\n\n---"
    },
    {
      "level": 2,
      "heading": "8. قوانین نهایی",
      "content": "| قانون | توضیح |\n|---|---|\n| تست در CI | تست‌ها در CI روی هر PR اجرا می‌شوند |\n| شکست تست = مسدود | در صورت شکست تست‌ها، merge مسدود می‌شود |\n| دیتابیس ایزوله | تست‌های یکپارچه از دیتابیس ایزوله استفاده می‌کنند |\n| بدون mock دیتابیس | در تست یکپارچه — دیتابیس واقعی |\n| بدون state sharing | هرگز وضعیت بین تست‌ها به اشتراک گذاشته نشود |\n| تست‌ها سریع | واحد < ۱۰۰ms, یکپارچه < ۵s |\n\n---"
    },
    {
      "level": 2,
      "heading": "9. الگوی تست سرویس‌های مالی",
      "content": ""
    },
    {
      "level": 3,
      "heading": "Integration Test با TigerBeetle",
      "content": "برای تست‌های یکپارچه با TigerBeetle، از یک نمونه TigerBeetle واقعی (یا embedded) استفاده کنید:\n\n```typescript\n// integration/tigerbeetle.test.ts\ndescribe('TigerBeetle Ledger - Integration', () => {\n  it('should record double-entry transaction', async () => {\n    const debitAccount = await tigerbeetle.createAccount({ type: 'buyer_wallet' })\n    const creditAccount = await tigerbeetle.createAccount({ type: 'escrow' })\n\n    const transfer = await tigerbeetle.createTransfer({\n      debitAccountId: debitAccount.id,\n      creditAccountId: creditAccount.id,\n      amount: 1200n,      // BigInt — integer, نه float\n      ledger: 1,          // 1 = USD\n      code: 10,           // نوع تراکنش\n    })\n\n    expect(transfer.id).toBeDefined()\n    const balance = await tigerbeetle.getAccountBalance(debitAccount.id)\n    expect(balance.debits).toBe(1200n)\n    expect(balance.credits).toBe(0n)\n  })\n\n  it('should fail on insufficient funds', async () => {\n    await expect(\n      tigerbeetle.createTransfer({\n        debitAccountId: emptyAccount.id,\n        creditAccountId: someAccount.id,\n        amount: 999999n,\n      })\n    ).rejects.toThrow('INSUFFICIENT_FUNDS')\n  })\n})\n```\n\n**قوانین:**\n- از `BigInt` برای مبالغ استفاده کنید — `float` برای پول ممنوع است\n- هر تست یک `debit_account_id` و `credit_account_id` یکتا داشته باشد\n- پس از تست، حساب‌های تست پاک شوند"
    },
    {
      "level": 3,
      "heading": "Mock Currency Conversion",
      "content": "برای تست واحد سرویس‌هایی که از currency-service استفاده می‌کنند:\n\n```typescript\n// unit/currency-client.mock.ts\nexport const createMockCurrencyClient = () => ({\n  convert: jest.fn().mockResolvedValue({\n    result: 1020000,\n    rate: 85000,\n    rateAt: '2024-01-01T12:00:00Z',\n  }),\n  getRates: jest.fn().mockResolvedValue({\n    USD_IRR: 85000,\n    updatedAt: '2024-01-01T12:00:00Z',\n  }),\n})\n```\n\n**سناریوهای تست:**\n\n| سناریو | رفتار مورد انتظار |\n|--------|-------------------|\n| تبدیل موفق | مبلغ صحیح با rounding درست برگردانده شود |\n| جفت ارز نامعتبر | خطای `UNSUPPORTED_PAIR` |\n| سرویس در دسترس نیست | fallback به cache — لاگ هشدار |\n| cache + سرویس هردو در دسترس نیستند | خطای `RATE_UNAVAILABLE` — توقف پرداخت |"
    },
    {
      "level": 3,
      "heading": "تست Idempotency در Settlement",
      "content": "settlement-service باید idempotent باشد — هر `settlement_id` فقط یک بار پردازش شود:\n\n```typescript\n// integration/settlement-idempotency.test.ts\ndescribe('Settlement Idempotency', () => {\n  it('should process same settlement only once', async () => {\n    const settlementId = 'stl_001'\n\n    // اولین بار — موفق\n    const first = await settlementService.settle({\n      settlementId,\n      sellerId: 'seller_1',\n      amountUsdCents: 1200,\n    })\n    expect(first.status).toBe('completed')\n\n    // ارسال مجدد با same ID — نادیده گرفته شود\n    const second = await settlementService.settle({\n      settlementId,\n      sellerId: 'seller_1',\n      amountUsdCents: 1200,\n    })\n    expect(second.status).toBe('duplicated')\n  })\n})\n```"
    }
  ]
}