{"id":"l-tj-5-4","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تشغّل مجموعة اختبارات متعدّدة الطبقات في CI باستخدام GitHub Actions كي يحصل كل pull request على تغذية راجعة سريعة وموثوقة، وتقرّر أيّ الاختبارات تتخطّاها أو تحذفها لأن تكلفتها أكبر مما تلتقطه.","html":"<p>الاختبارات على حاسوبك تحميك أنت. أما الاختبارات في CI (التكامل المستمر) فتحمي الفريق كله، مع كل pull request، سواء تذكّر أحدهم تشغيلها أم لا. يجمع هذا الدرس الأخير القطع معًا: pipeline يشغّل الطبقات التي بنيتها بالترتيب الصحيح، وحسن التقدير في ما يستحق اختبارًا من الأساس.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">pipeline من الأسرع إلى الأبطأ</h2>\n<p>الترتيب مهم بقدر المحتوى. الفحوص الرخيصة التي تفشل كثيرًا تأتي أولًا، فيكلّفك الخطأ المطبعي 30 ثانية من الانتظار بدل 12 دقيقة:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 170\" role=\"img\" aria-labelledby=\"l-tj-5-4-t1\"><defs><marker id=\"arrow-l-tj-5-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-tj-5-4-t1\">أربع مراحل من اليسار إلى اليمين: الـ lint والأنواع في ثوانٍ، واختبارات unit وintegration في دقيقة تقريبًا، والبناء، ثم اختبارات end-to-end موزّعة في بضع دقائق. كل مرحلة لا تعمل إلا إن نجحت التي قبلها.</title>\n  <rect class=\"d-box-warn\" x=\"10\" y=\"50\" width=\"145\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"82\" y=\"80\" text-anchor=\"middle\">Lint + أنواع</text>\n  <text class=\"d-label-muted\" x=\"82\" y=\"104\" text-anchor=\"middle\">~30 s</text>\n  <path class=\"d-arrow\" d=\"M155 85 L183 85\" marker-end=\"url(#arrow-l-tj-5-4-1)\"/>\n  <rect class=\"d-box-accent\" x=\"185\" y=\"50\" width=\"145\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"257\" y=\"80\" text-anchor=\"middle\">Vitest</text>\n  <text class=\"d-label-muted\" x=\"257\" y=\"104\" text-anchor=\"middle\">~1 min</text>\n  <path class=\"d-arrow\" d=\"M330 85 L358 85\" marker-end=\"url(#arrow-l-tj-5-4-1)\"/>\n  <rect class=\"d-box\" x=\"360\" y=\"50\" width=\"120\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"420\" y=\"80\" text-anchor=\"middle\">البناء</text>\n  <text class=\"d-label-muted\" x=\"420\" y=\"104\" text-anchor=\"middle\">~30 s</text>\n  <path class=\"d-arrow\" d=\"M480 85 L508 85\" marker-end=\"url(#arrow-l-tj-5-4-1)\"/>\n  <rect class=\"d-box-primary\" x=\"510\" y=\"50\" width=\"160\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"590\" y=\"80\" text-anchor=\"middle\">Playwright</text>\n  <text class=\"d-label-muted\" x=\"590\" y=\"104\" text-anchor=\"middle\">3 shards، ~4 min</text>\n  <text class=\"d-label-muted\" x=\"340\" y=\"155\" text-anchor=\"middle\">الفشل يوقف التشغيل مبكرًا</text>\n</svg><figcaption>pipeline لـ pull request، الفحوص الأرخص أولًا</figcaption></figure>\n<p>وإليك ذلك على شكل workflow في GitHub Actions:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">.github/workflows/test.yml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">Test</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  pull_request</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  push</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    branches</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">main</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">jobs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  unit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    runs-on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ubuntu-latest</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    steps</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/checkout@v6</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/setup-node@v6</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          node-version</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">lts/*</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          cache</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm ci</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm run lint</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npx tsc --noEmit</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npx vitest run --coverage</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  e2e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    needs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">unit</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    runs-on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ubuntu-latest</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    strategy</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      fail-fast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      matrix</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        shard</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    steps</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/checkout@v6</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/setup-node@v6</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          node-version</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">lts/*</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          cache</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm ci</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npx playwright install --with-deps</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npx playwright test --shard=${{ matrix.shard }}/3</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/upload-artifact@v4</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">${{ !cancelled() }}</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">playwright-report-${{ matrix.shard }}</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">playwright-report/</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          retention-days</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">14</span></span></code></pre></div>\n<p>بضعة تفاصيل تؤدّي عملًا حقيقيًا:</p>\n<ul>\n<li><strong><code dir=\"ltr\">needs: unit</code></strong> يعني أن مهمّة الـ end-to-end لا تبدأ إلا حين تنجح اختبارات الوحدات. لا فائدة من تشغيل المتصفّحات لكود يفشل في <code dir=\"ltr\">tsc</code>.</li>\n<li><strong><code dir=\"ltr\">--shard=1/3</code></strong> يقسّم مجموعة Playwright على ثلاثة أجهزة تعمل بالتوازي. (وللحصول على تقرير واحد مدمج، انتقل إلى المُبلّغ <code dir=\"ltr\">blob</code> وشغّل <code dir=\"ltr\">npx playwright merge-reports</code> في مهمّة أخيرة.)</li>\n<li><strong><code dir=\"ltr\">if: ${{ !cancelled() }}</code></strong> يرفع التقرير حتى حين تفشل الاختبارات، وهذا بالضبط حين تحتاج إلى الـ traces من <a href=\"/ar/lesson/testing-javascript/l-tj-4-3\">درس تتبّع الأخطاء</a>.</li>\n<li><strong><code dir=\"ltr\">CI</code> يُضبط تلقائيًا</strong> على خوادم GitHub، فتعمل Vitest مرة واحدة بدل المراقبة، ويفشل أي <code dir=\"ltr\">.only</code> شارد، ويبدأ عمل <code dir=\"ltr\">retries: process.env.CI ? 2 : 0</code> في إعدادات Playwright.</li>\n</ul>\n<p>وأخيرًا، في إعدادات المستودع، اجعل هذه المهامّ <strong>فحوص حالة إلزامية</strong> (required status checks) على <code dir=\"ltr\">main</code>، كي يمنع الـ pipeline الأحمر الـ merge فعلًا. فالـ pipeline الذي لا يُلزَم أحد بالنجاح فيه مجرّد اقتراح.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>أبقِه تحت عشر دقائق</span></p>\n<p>ينتظر الناس pipeline يستغرق خمس دقائق. وينتقلون إلى عمل آخر مع pipeline يستغرق عشرين، ويبدؤون بالـ merge متجاوزين pipeline يستغرق أربعين. تعامل مع وقت الـ pipeline كميزانية: وزّع بالـ sharding، وخزّن الاعتماديات مؤقتًا، وشغّل مجموعة Vitest السريعة قبل أي شيء بطيء، وانقل الفحوص إلى أسفل الكأس حين يكرّر اختبار end-to-end ما يفعله unit test.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">اختيار ما لا تختبره</h2>\n<p>كل اختبار يكلّف وقت كتابة، ودقائق CI، وصيانة. وبعضها يكلّف أكثر مما سيلتقطه في أي وقت:</p>\n<ul>\n<li><strong>الكود التافه</strong>: دالة getter تعيد حقلًا، أو ثابت (<code dir=\"ltr\">expect(CURRENCY).toBe('USD')</code> يعيد قول ما في الكود).</li>\n<li><strong>سلوك أطر العمل والمكتبات</strong>: أن <code dir=\"ltr\">useState</code> يحدّث الـ state، أو أن <code dir=\"ltr\">Array.prototype.sort</code> يرتّب، أو أن مكتبة التواريخ لديك تنسّق التواريخ. هذا يختبره القائمون على صيانتها.</li>\n<li><strong>التفاصيل الداخلية</strong>: أيّ دالة مساعدة خاصة استُدعيت، وكم مرة أُعيد render مكوّن ما، والحالة الداخلية. هذه تفشل مع إعادة الهيكلة وتنجح مع الأخطاء.</li>\n<li><strong>التنسيق</strong>: أسماء الأصناف وقيم البكسل في الـ unit tests. وإن كانت الانتكاسات البصرية مهمّة، فاستخدم اختبارات لقطات الشاشة في متصفّح حقيقي لبضع شاشات أساسية.</li>\n<li><strong>الكود المؤقّت</strong>: التجارب والنماذج الأولية التي توشك على حذفها.</li>\n</ul>\n<p>وكن مستعدًا <strong>للحذف</strong>. الاختبار الذي لم يفشل قط لسبب حقيقي خلال عامين، أو الذي ينكسر مع كل إعادة هيكلة، أو المتخطّى بشكل دائم، عبء. وحذفه مساهمة.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>اختبار كل شيء في المستوى نفسه</span></p>\n<p>مجموعة فيها 400 اختبار end-to-end و20 unit test بطيئة ومتقلّبة وصعبة التتبّع؛ ومجموعة فيها 2,000 unit test بلا أي اختبار end-to-end قد تنجح بينما لا يُحمَّل التطبيق أصلًا. لا هذه ولا تلك “اختبار أكثر”. ضع كل فحص في أدنى مستوى يستطيع التقاط الخطأ، واحتفظ بحفنة من اختبارات الـ end-to-end للمسارات التي تربط كل شيء معًا.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">الصورة الكاملة</h2>\n<p>لديك الآن كل الطبقات: الفحوص الساكنة، والـ unit tests التي تثبّت السلوك والحدود، واختبارات الواجهة التي تتصرّف كالمستخدمين، واختبارات الـ end-to-end التي تنتظر بشكل صحيح وتبقى معزولة، والأدوات للحكم عليها: الـ coverage للفجوات، والـ mutation testing للقوة. والسؤال الذي يقف خلف كل ذلك هو نفسه سؤال الدرس الأول: <strong>هل سيفشل هذا الاختبار بسبب خطأ قد يصل فعلًا إلى المستخدمين، بسرعة، وبرسالة تقول ما الذي انكسر؟</strong> احتفظ بالاختبارات التي تجيب بنعم، وأصلح التي يمكن أن تجيب بها، واحذف البقية.</p>\n<p>يطلب منك التمرين اتخاذ هذه القرارات على قائمة اختبارات تبدو حقيقية.</p>\n","toc":[{"id":"sec-1","text":"pipeline من الأسرع إلى الأبطأ","level":2},{"id":"sec-2","text":"اختيار ما لا تختبره","level":2},{"id":"sec-3","text":"الصورة الكاملة","level":2}],"takeaways":["رتّب CI من الأسرع إلى الأبطأ (الـ lint والأنواع، ثم اختبارات unit وintegration، ثم end-to-end) كي توقف حالات الفشل الرخيصة التشغيل مبكرًا.","اجعل مهامّ الاختبار فحوصًا إلزامية (required checks) على الفرع الرئيسي، وارفع تقارير Playwright كـ artifacts، ووزّع مجموعات الـ end-to-end البطيئة على عدة أجهزة بالـ sharding.","أبقِ الـ pipeline كله سريعًا بما يكفي كي ينتظره الناس؛ فالـ pipeline الذي يستغرق 40 دقيقة يُتجاوَز.","لا تختبر الكود التافه، ولا سلوك أطر العمل أو المكتبات، ولا التنسيق، ولا التفاصيل الداخلية؛ واحذف الاختبارات التي لا تفشل أبدًا لسبب حقيقي.","يستحق الاختبار مكانه حين يفشل بسبب خطأ قد يصل فعلًا إلى المستخدمين، بسرعة، وبرسالة واضحة."],"further":[{"title":"Playwright — Continuous Integration","url":"https://playwright.dev/docs/ci-intro"},{"title":"Playwright — Sharding","url":"https://playwright.dev/docs/test-sharding"},{"title":"GitHub Docs — About protected branches","url":"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches"},{"title":"Vitest — Reporters","url":"https://vitest.dev/guide/reporters"}],"quiz":[{"q":"<p>يشغّل الـ pipeline لديك Playwright أولًا (12 دقيقة)، ثم الـ lint (20 ثانية). خطأ مطبعي في import يُفشل الـ lint في معظم الـ pull requests المعطوبة. ما الترتيب الأفضل؟</p>","options":[{"text":"أبقِ الترتيب؛ اختبارات الـ end-to-end هي الأهم، فيجب أن تعمل أولًا.","why":"الأهمية ليست المقصود؛ تشغيل أبطأ مهمّة أولًا يعني أن المطوّرين ينتظرون 12 دقيقة ليعرفوا بمشكلة تستغرق 20 ثانية."},{"text":"الـ lint وفحص الأنواع أولًا، ثم اختبارات unit وintegration، ثم end-to-end فقط إن نجح ما قبلها.","why":"صحيح. الفحوص السريعة الرخيصة تفشل مبكرًا وتوفّر دقائق التشغيل؛ والمرحلة البطيئة لا تعمل إلا على كود معقول أصلًا."},{"text":"شغّل الـ lint على الفرع الرئيسي فقط بعد الـ merge.","why":"عندها يصل الكود المعطوب إلى main قبل أن يلاحظه أحد، وهذا بالضبط ما وُجد CI على الـ pull requests لمنعه."}],"answer":1},{"q":"<p>أيّ من هذه الاختبارات هو أفضل مرشّح للحذف؟</p>","options":[{"text":"اختبار انتكاس لخطأ مبلَّغ عنه كانت فيه <code dir=\"ltr\">'12.5'</code> تُحلَّل على أنها 1205 سنتًا.","why":"اختبارات الانتكاس للأخطاء الحقيقية من أثمن الاختبارات التي تملكها؛ ذلك الخطأ حدث مرة ويمكن أن يحدث مجددًا."},{"text":"اختبار يعرض مكوّنًا ويتحقّق من أن دالة الضبط في <code dir=\"ltr\">useState</code> الخاص بـ React تحدّث الـ state.","why":"صحيح. إنه يختبر React، لا كودك. مجموعة اختبارات React نفسها تغطّي ذلك، ولا يمكن أن يفشل الاختبار إلا إن كانت React نفسها معطّلة."},{"text":"اختبار end-to-end يضيف مصروفًا ويتحقّق من أن الأرصدة تتحدّث.","why":"هذا مسار حرج يكون فيه الاتصال بين الواجهة والـ API والمنطق هو الخطر، وهو بالضبط ما وُجدت اختبارات الـ end-to-end من أجله."}],"answer":1},{"q":"<p>كبرت مجموعة الـ end-to-end حتى صارت تستغرق 30 دقيقة على جهاز واحد. ما الخطوة الأولى الأكثر فاعلية؟</p>","options":[{"text":"وزّعها على عدة أجهزة بـ <code dir=\"ltr\">--shard</code>، وتحقّق إن كانت بعض اختبارات الـ end-to-end تكرّر اختبارات في مستويات أدنى.","why":"صحيح. الـ sharding يقلّص الوقت الفعلي فورًا؛ ونقل الفحوص المكرّرة إلى أسفل الكأس يقلّص العمل الكلي."},{"text":"شغّل مجموعة الـ end-to-end مرة واحدة في الأسبوع فقط.","why":"عندها تتراكم تغييرات أسبوع كامل قبل أن يعرف أحد أن مسارًا حرجًا انكسر، ويصبح إيجاد المتسبّب أصعب بكثير."},{"text":"زِد مهلة كل اختبار كي يفشل عدد أقل منها.","why":"المهل ليست المشكلة هنا، والمهل الأطول تجعل المجموعة البطيئة أبطأ حين يسوء شيء ما فعلًا."}],"answer":0}],"exercise":{"kind":"choice","multiple":true,"options":["page-snapshot","negative-format","usestate","settle-e2e","constant","private-spy","regression","network-404"],"correct":["page-snapshot","usestate","constant","private-spy"],"title":"تحتفظ به أم تحذفه؟","prompt":"<p>ورثت مجموعة اختبارات Splitwise-lite. يستغرق CI خمسًا وعشرين دقيقة، والفريق يريده أسرع دون أن يفقد الثقة. اختر <strong>كل اختبار ستحذفه</strong> (أو لن تكتبه من الأساس). واترك دون اختيار الاختبارات التي تستحق مكانها.</p>\n","hints":["لكل اختبار، اسأل: أيّ خطأ معقول سيجعله يفشل؟ إن كانت الإجابة “إعادة هيكلة فقط” أو “خطأ في React فقط”، فهو مرشّح للحذف.","اختبارات الانتكاس للأخطاء الحقيقية واختبارات الحالات الحدّية في المال نادرًا ما تكون هي التي تُحذف."],"explanation":"<p>احذف أربعة. الـ snapshot للصفحة كلها يفشل مع كل تعديل في البنية ويعلّم الناس الضغط على “update” دون قراءة. واختبار <code dir=\"ltr\">useState</code> يفحص React، لا Splitwise-lite. واختبار الثابت يعيد قول سطر من الكود، ولا يمكن أن يفشل إلا حين يغيّر أحدهم القيمة عن قصد. والـ spy على دالة مساعدة خاصة يفشل مع إعادة الهيكلة وينجح مع الأخطاء الحقيقية. واحتفظ بالأربعة الأخرى: تنسيق المبلغ السالب حالة حدّية في المال؛ واختبار الـ end-to-end للتسوية يغطّي المسار الذي تلتقي فيه الواجهة والـ API والمنطق؛ واختبار الانتكاس يوثّق خطأً وصل فعلًا إلى المستخدمين؛ واختبار 404 يغطّي معالجة أخطاء يصادفها المستخدمون مع الروابط المعطوبة. ولتسريع CI، اجمع بين هذا الحذف والـ sharding ونقل فحوص الـ end-to-end المكرّرة إلى اختبارات المكوّنات.</p>\n","checkLabels":{},"optionText":{"page-snapshot":{"text":"<code dir=\"ltr\">expect(container).toMatchSnapshot()</code> على صفحة المجموعة المعروضة كاملة (snapshot من 400 سطر).","why":"احذفه. يفشل مع كل تغيير غير ضارّ في البنية ولا يقول شيئًا عن السلوك المهم، فيوافق المراجعون على التحديثات دون تمحيص."},"negative-format":{"text":"<code dir=\"ltr\">expect(formatMoney(-505)).toBe('-$5.05')</code>","why":"احتفظ به. المبالغ السالبة وإكمال الصفر هي بالضبط مواضع انكسار تنسيق المال، والاختبار يعمل في جزء من الثانية."},"usestate":{"text":"اختبار يعرض مكوّنًا، ويستدعي دالة الضبط في <code dir=\"ltr\">useState</code> عبر hook اختبار، ويتحقّق من أن الـ state تغيّر.","why":"احذفه. يختبر سلوك React نفسها، الذي تغطّيه مجموعة اختبارات React أصلًا؛ ولا يمكن أن يفشل بسبب كودك."},"settle-e2e":{"text":"اختبار Playwright: أضف مصروفًا، وانقر Settle up، وتوقّع “Everyone is settled up”.","why":"احتفظ به. إنه المسار الأساسي، ولا يتحقّق من أن الواجهة والـ API ومنطق التسوية تعمل معًا إلا اختبار end-to-end."},"constant":{"text":"<code dir=\"ltr\">expect(DEFAULT_CURRENCY).toBe('USD')</code>","why":"احذفه. يعيد قول الكود المصدري. لا يفشل إلا حين يغيّر أحدهم الثابت عمدًا، وعندها يحتاج هو أيضًا إلى تحديث فحسب."},"private-spy":{"text":"اختبار يتجسّس على الدالة المساعدة الداخلية <code dir=\"ltr\">_sortDebtors</code> ويؤكّد أن <code dir=\"ltr\">settleUp</code> استدعتها مرة واحدة بالضبط.","why":"احذفه. يجمّد تفصيلًا داخليًا: دمج الدالة المساعدة أو إعادة تسميتها يكسره، بينما تنجح معه تسوية خاطئة."},"regression":{"text":"اختبار انتكاس من تقرير خطأ الربيع الماضي: <code dir=\"ltr\">expect(parseAmount('12.5')).toBe(1250)</code>.","why":"احتفظ به. هذا الخطأ وصل فعلًا إلى المستخدمين مرة؛ والاختبار يضمن ألا يعود بصمت أبدًا."},"network-404":{"text":"اختبار يتحقّق من أن <code dir=\"ltr\">loadGroup</code> تُرفض بالرسالة “HTTP 404” حين يجيب الـ API بـ 404.","why":"احتفظ به. معالجة الأخطاء هي حيث تختبئ الأخطاء، والروابط المعطوبة أو القديمة تجعل 404 مسارًا حقيقيًا للمستخدم."}}},"words":572,"readMinutes":3}