{"id":"l-tj-5-2","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تقيس الـ coverage (نسبة التغطية) في Vitest، وتقرأ التقرير لتجد الكود غير المختبَر، وتشرح لماذا قد يُخفي رقم تغطية مرتفع مجموعة اختبارات ضعيفة.","html":"<p>كل فريق يضيف اختبارات يسأل في النهاية “كم يكفي؟”، والـ coverage هي الرقم الذي يبدو أنه يجيب. لكنها تجيب عن سؤال أضيق مما يظنّه الناس. إن استُخدمت جيدًا، فهي طريقة سريعة لإيجاد كود لم يختبره أحد. وإن استُخدمت كدرجة تقييم، فهي تكافئ الاختبارات الخاطئة.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">تشغيلها</h2>\n<p>تدعم Vitest مزوّدَين للـ coverage. الافتراضي، <code dir=\"ltr\">v8</code>، يستخدم التغطية المدمجة في محرّك JavaScript ولا يحتاج إلى خطوة تجهيز للكود (instrumentation)؛ أما <code dir=\"ltr\">istanbul</code> فيجهّز الكود ويعمل في بيئات تشغيل أكثر. ثبّت المزوّد وشغّل مع <code dir=\"ltr\">--coverage</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</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:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --save-dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @vitest/coverage-v8</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> vitest</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --coverage</span></span></code></pre></div>\n<p>اضبط ما تقيسه وطريقة الإبلاغ عنه:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">vite.config.js</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:#6E7781;--shiki-dark:#8B949E\">/// &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">reference</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> types</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"vitest/config\"</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\"> /></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { defineConfig } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'vite'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> defineConfig</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  test: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    coverage: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      provider: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'v8'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      include: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'src/**/*.{js,jsx}'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      exclude: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'src/**/*.test.{js,jsx}'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'src/main.jsx'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      reporter: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'text'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'html'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      thresholds: { lines: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">80</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, branches: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">75</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">})</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n<p><code dir=\"ltr\">include</code> مهم: في Vitest 4، بدونه لا يسرد التقرير إلا الملفات التي صادف أن حمّلتها اختباراتك، فلا تظهر الوحدة غير المختبَرة كليًا أصلًا، وتبدو النسبة أفضل مما هي. ومع <code dir=\"ltr\">include</code>، تظهر الملفات غير المختبَرة بنسبة 0%. ويكتب مُبلّغ <code dir=\"ltr\">html</code> الملف <code dir=\"ltr\">coverage/index.html</code>، الذي تفتحه في المتصفّح.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">الأرقام الأربعة</h2>\n<ul>\n<li><strong>Statements (العبارات)</strong>: كم عبارة عملت.</li>\n<li><strong>Branches (الفروع)</strong>: كم جهة من كل قرار عملت (<code dir=\"ltr\">if</code>/<code dir=\"ltr\">else</code>، و<code dir=\"ltr\">? :</code>، و<code dir=\"ltr\">&amp;&amp;</code>، و<code dir=\"ltr\">||</code>، و<code dir=\"ltr\">??</code>، والمعاملات الافتراضية).</li>\n<li><strong>Functions (الدوال)</strong>: كم دالة استُدعيت مرة واحدة على الأقل.</li>\n<li><strong>Lines (الأسطر)</strong>: كم سطرًا عمل فيه شيء ما.</li>\n</ul>\n<p>تغطية الفروع هي ما أنظر إليه. <code dir=\"ltr\">const share = people &gt; 0 ? total / people : 0;</code> سطر واحد وفرعان. اختبار بثلاثة أشخاص يجعل السطر مغطّى بنسبة 100% والفروع بنسبة 50%، والفرع غير المختبَر هو بالضبط الحالة الحدّية من القسم 2.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">قراءة التقرير</h2>\n<p>يعلّم تقرير HTML الأسطر التي لم تعمل قط بالأحمر، والفروع التي سُلكت في اتجاه واحد فقط بالأصفر. وهذا هو الجزء الثمين: <strong>الكود الأحمر غير مختبَر، بكل تأكيد.</strong> لا يمكن لأي assertion في أي مكان أن يكون قد فحصه، لأنه لم يعمل قط. حين أنضمّ إلى مشروع، يكون تقرير الـ coverage أسرع خريطة لأماكن الخطر: معالجة الأخطاء غير المغطّاة في وحدة الدفع، وفرع <code dir=\"ltr\">else</code> الذي لم يصل إليه أحد.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 230\" role=\"img\" aria-labelledby=\"l-tj-5-2-t1\"><defs><marker id=\"arrow-l-tj-5-2-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-2-t1\">صندوق كبير لكل الكود؛ بداخله صندوق للكود الذي نفّذته الاختبارات؛ وبداخل ذلك صندوق أصغر للكود الذي تتحقّق الاختبارات فعلًا من نتائجه. تُبلّغ الـ coverage عن الصندوق الأوسط.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"16\" width=\"600\" height=\"200\" rx=\"16\"/>\n  <text class=\"d-label-strong\" x=\"40\" y=\"44\">كل الكود</text>\n  <rect class=\"d-box-accent\" x=\"120\" y=\"60\" width=\"440\" height=\"140\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"140\" y=\"88\">نفّذته الاختبارات</text>\n  <text class=\"d-label-muted\" x=\"400\" y=\"88\">ما تُبلّغ عنه الـ coverage</text>\n  <rect class=\"d-box-success\" x=\"200\" y=\"106\" width=\"250\" height=\"78\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"325\" y=\"140\" text-anchor=\"middle\">نتائج مُتحقَّق منها</text>\n  <text class=\"d-label-muted\" x=\"325\" y=\"164\" text-anchor=\"middle\">ما تحتاج إليه</text>\n</svg><figcaption>ترى الـ coverage ما عمل، لا ما فُحص</figcaption></figure>\n<h2 id=\"sec-4\" tabindex=\"-1\">ما لا تستطيع الـ coverage رؤيته</h2>\n<p>تسجّل الـ coverage <strong>التنفيذ</strong>. ولا تعرف شيئًا عمّا إذا كان أي شيء قد فحص النتيجة. هذا الاختبار يجعل <code dir=\"ltr\">computeBalances</code> مغطّاة بنسبة 100%:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</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:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'computes balances'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> balances</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> computeBalances</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    { paidBy: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ana'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, splitAmong: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ana'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ben'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Cai'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ]);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">keys</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(balances)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveLength</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:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>عمل كل سطر. وسينجح الـ assertion حتى لو خُصم المبلغ من الدافع بدل أن يُضاف إليه، أو أُخذ الجزء الصحيح من الحصص فاختفى سنت، أو استبدل مصروف ثانٍ الأول. الـ coverage: 100%. الثقة: قريبة من الصفر. ويبدأ التمرين من هذا الاختبار بالضبط.</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>الـ coverage كهدف</span></p>\n<p>حين يُقاس الفريق بالرقم، يرتفع الرقم، وأسهل طريق للارتفاع هو اختبارات كالتي في الأعلى: استدعِ كل شيء، وأكّد القليل. هذا قانون Goodhart: المقياس الذي يصبح هدفًا يتوقّف عن أن يكون مقياسًا جيدًا. اضبط حدًّا أدنى أقل بقليل من مستواك الحالي، <strong>كأرضية</strong> تمنع الـ coverage من الانزلاق حين يضيف أحدهم كودًا غير مختبَر، ولا تجعله أبدًا هدفًا تطارده.</p>\n</aside>\n<p>السطر غير المغطّى سؤال، لا أمر. أحيانًا تكون الإجابة الصحيحة اختبارًا. وأحيانًا تكون حذف الكود: فرع لا يستطيع أحد الوصول إليه، أو بديل احتياطي لمتصفّح توقّفت عن دعمه، أو دالة مساعدة لم يعد شيء يستدعيها. والكود الميت “المغطّى” باختبار كُتب فقط لرفع الرقم هو أسوأ النتائج، لأنه يبدو الآن مهمًّا.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">كم يكفي؟</h2>\n<p>لا يوجد رقم عالمي. لكن هذه قواعد تقريبية مفيدة:</p>\n<ul>\n<li>استهدف <strong>تغطية فروع عالية للمنطق الأساسي</strong> (المال، والصلاحيات، وتحويلات البيانات)، واقبل بأقل لكود الربط وتوصيلات الواجهة التي تشغّلها اختبارات الـ end-to-end على أي حال.</li>\n<li>استثنِ الكود الذي لا تكتب له unit tests عن قصد (نقاط الدخول، والملفات المولّدة، والوحدات التي تحتوي أنواعًا فقط) كي يكون للرقم معنى.</li>\n<li>انظر إلى الـ coverage <strong>على الـ diff</strong> في مراجعة الكود: هل أضاف هذا التغيير فروعًا غير مختبَرة؟ وهذا أنفع عمليًا من نسبة على مستوى المشروع كله.</li>\n</ul>\n<p>تخبرك الـ coverage أين تنقص الاختبارات. ولتعرف إن كانت الاختبارات الموجودة جيدة أصلًا، تحتاج إلى أداة مختلفة، وهي موضوع الدرس القادم. لكن أولًا، في التمرين، حوّل مجموعة تغطيتها 100% ولا تلتقط شيئًا إلى مجموعة تلتقط أخطاءً حقيقية.</p>\n","toc":[{"id":"sec-1","text":"تشغيلها","level":2},{"id":"sec-2","text":"الأرقام الأربعة","level":2},{"id":"sec-3","text":"قراءة التقرير","level":2},{"id":"sec-4","text":"ما لا تستطيع الـ coverage رؤيته","level":2},{"id":"sec-5","text":"كم يكفي؟","level":2}],"takeaways":["تقيس الـ coverage أيّ العبارات والفروع والدوال والأسطر عملت أثناء الاختبارات، لا إن كان شيء قد فحص النتائج.","أنفع ما في تقرير الـ coverage هو الأحمر والأصفر اللذان يُظهرهما لك؛ فالكود غير المغطّى كود غير مختبَر بكل تأكيد.","تغطية الفروع (branch coverage) أكثر صرامة وأنفع من تغطية الأسطر، لأن السطر الواحد قد يحمل عدة قرارات.","استخدم الحدود الدنيا (thresholds) كأرضية تمنع التغطية من الانزلاق، لا كهدف تسعى إليه؛ فالرقم الذي يصبح هدفًا يُتلاعب به."],"further":[{"title":"Vitest — Coverage guide","url":"https://vitest.dev/guide/coverage"},{"title":"Vitest — Coverage config","url":"https://vitest.dev/config/coverage"}],"quiz":[{"q":"<p>يستدعي اختبار <code dir=\"ltr\">computeBalances(expenses)</code> ولا يؤكّد إلا <code dir=\"ltr\">expect(result).toBeDefined()</code>. الـ coverage للملف 100%. ماذا يخبرك ذلك؟</p>","options":[{"text":"الدالة مختبَرة بالكامل.","why":"كل سطر عمل، لكن لا شيء فحص الأرقام؛ وخطأ في الإشارة كان سينجح رغم ذلك."},{"text":"كل سطر عمل أثناء الاختبارات، ولا شيء عن صحّة النتائج.","why":"صحيح. الـ coverage تقيس التنفيذ. وجودة الـ assertions غير مرئية لها."},{"text":"أخطأت Vitest في العدّ، لأن 100% مستحيلة مع اختبار واحد.","why":"يستطيع اختبار واحد بسهولة تنفيذ كل سطر في دالة صغيرة؛ ولهذا بالضبط تكون الـ coverage وحدها إشارة ضعيفة."}],"answer":1},{"q":"<p>تغطية الأسطر 100% لكن تغطية الفروع 50% للسطر <code dir=\"ltr\">const share = people &gt; 0 ? total / people : 0;</code>. ما الناقص؟</p>","options":[{"text":"اختبار تكون فيه <code dir=\"ltr\">people</code> مساوية 0 (أو سالبة)، كي تعمل الجهة الأخرى من الشرط.","why":"صحيح. عمل السطر، لكن فرعًا واحدًا فقط من فرعيه عمل؛ تغطية الفروع تلاحظ ذلك، وتغطية الأسطر لا تستطيع."},{"text":"اختبار ثانٍ بقيمة <code dir=\"ltr\">total</code> أكبر.","why":"المجموع المختلف يسلك الفرع نفسه، فتبقى تغطية الفروع عند 50%."},{"text":"لا شيء؛ تغطية الفروع دائمًا أقل من تغطية الأسطر.","why":"كثيرًا ما تكون أقل، لكنها هنا تشير إلى قرار محدّد غير مختبَر."}],"answer":0},{"q":"<p>يضع فريقك حدًّا أدنى صارمًا للـ coverage قدره 90% ويجعله هدفًا ربع سنوي. ما الأثر الجانبي الأرجح؟</p>","options":[{"text":"أخطاء أقل، لأن 90% من الكود صار مُتحقَّقًا منه الآن.","why":"الـ coverage تعدّ التنفيذ لا التحقّق، فقد يرتفع الرقم دون أن تقلّ الأخطاء."},{"text":"CI أبطأ، لأن جمع الـ coverage مكلف.","why":"هناك عبء إضافي، لكنه ليس الخطر الأساسي في تحويل الرقم إلى هدف."},{"text":"يكتب الناس اختبارات تنفّذ الكود دون assertions ذات معنى، فيرتفع الرقم ولا ترتفع الثقة.","why":"صحيح. بمجرد أن يصبح المقياس هدفًا، يُحسَّن مباشرة؛ استخدم الحدود الدنيا كأرضية، واقرأ التقرير بحثًا عن الفجوات بدلًا من ذلك."}],"answer":2}],"exercise":{"kind":"web","mode":"test","subject":"function splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n\n// Computes each person's balance in cents from a list of expenses.\n// Positive: the group owes them. Negative: they owe the group.\n// Each expense is { paidBy, amountCents, splitAmong: [names] }.\n// - The payer is credited the full amount.\n// - Each person in splitAmong is debited their share from splitEvenly\n//   (leftover cents go to the first names in the list).\n// - Balances accumulate across expenses and always add up to zero.\nfunction computeBalances(expenses) {\n  const balances = {};\n  for (const { paidBy, amountCents, splitAmong } of expenses) {\n    const shares = splitEvenly(amountCents, splitAmong.length);\n    balances[paidBy] = (balances[paidBy] ?? 0) + amountCents;\n    splitAmong.forEach((name, i) => {\n      balances[name] = (balances[name] ?? 0) - shares[i];\n    });\n  }\n  return balances;\n}\n","mutants":[{"id":"payer-debited","code":"function splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n\n// Computes each person's balance in cents from a list of expenses.\n// Positive: the group owes them. Negative: they owe the group.\n// Each expense is { paidBy, amountCents, splitAmong: [names] }.\n// - The payer is credited the full amount.\n// - Each person in splitAmong is debited their share from splitEvenly\n//   (leftover cents go to the first names in the list).\n// - Balances accumulate across expenses and always add up to zero.\nfunction computeBalances(expenses) {\n  const balances = {};\n  for (const { paidBy, amountCents, splitAmong } of expenses) {\n    const shares = splitEvenly(amountCents, splitAmong.length);\n    balances[paidBy] = (balances[paidBy] ?? 0) - amountCents;\n    splitAmong.forEach((name, i) => {\n      balances[name] = (balances[name] ?? 0) - shares[i];\n    });\n  }\n  return balances;\n}\n"},{"id":"floored-shares","code":"function splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n\n// Computes each person's balance in cents from a list of expenses.\n// Positive: the group owes them. Negative: they owe the group.\n// Each expense is { paidBy, amountCents, splitAmong: [names] }.\n// - The payer is credited the full amount.\n// - Each person in splitAmong is debited their share from splitEvenly\n//   (leftover cents go to the first names in the list).\n// - Balances accumulate across expenses and always add up to zero.\nfunction computeBalances(expenses) {\n  const balances = {};\n  for (const { paidBy, amountCents, splitAmong } of expenses) {\n    const shares = splitEvenly(amountCents, splitAmong.length);\n    balances[paidBy] = (balances[paidBy] ?? 0) + amountCents;\n    splitAmong.forEach((name, i) => {\n      balances[name] = (balances[name] ?? 0) - Math.floor(amountCents / splitAmong.length);\n    });\n  }\n  return balances;\n}\n"},{"id":"overwrites-payer","code":"function splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n\n// Computes each person's balance in cents from a list of expenses.\n// Positive: the group owes them. Negative: they owe the group.\n// Each expense is { paidBy, amountCents, splitAmong: [names] }.\n// - The payer is credited the full amount.\n// - Each person in splitAmong is debited their share from splitEvenly\n//   (leftover cents go to the first names in the list).\n// - Balances accumulate across expenses and always add up to zero.\nfunction computeBalances(expenses) {\n  const balances = {};\n  for (const { paidBy, amountCents, splitAmong } of expenses) {\n    const shares = splitEvenly(amountCents, splitAmong.length);\n    balances[paidBy] = amountCents;\n    splitAmong.forEach((name, i) => {\n      balances[name] = (balances[name] ?? 0) - shares[i];\n    });\n  }\n  return balances;\n}\n"}],"starter":{"js":"// This suite gives computeBalances 100% line coverage.\n// It also catches almost nothing. Strengthen it.\ndescribe('computeBalances', () => {\n  test('computes balances', () => {\n    const balances = computeBalances([\n      { paidBy: 'Ana', amountCents: 3000, splitAmong: ['Ana', 'Ben', 'Cai'] },\n    ]);\n\n    expect(Object.keys(balances)).toHaveLength(3);\n  });\n});\n"},"solution":{"js":"const sum = (balances) => Object.values(balances).reduce((a, b) => a + b, 0);\n\ndescribe('computeBalances', () => {\n  test('credits the payer and debits everyone their share', () => {\n    const balances = computeBalances([\n      { paidBy: 'Ana', amountCents: 3000, splitAmong: ['Ana', 'Ben', 'Cai'] },\n    ]);\n\n    expect(balances).toEqual({ Ana: 2000, Ben: -1000, Cai: -1000 });\n  });\n\n  test('balances add up to zero when the bill does not divide evenly', () => {\n    const balances = computeBalances([\n      { paidBy: 'Ana', amountCents: 1000, splitAmong: ['Ana', 'Ben', 'Cai'] },\n    ]);\n\n    expect(balances).toEqual({ Ana: 666, Ben: -333, Cai: -333 });\n    expect(sum(balances)).toBe(0);\n  });\n\n  test('accumulates several expenses paid by the same person', () => {\n    const balances = computeBalances([\n      { paidBy: 'Ana', amountCents: 2000, splitAmong: ['Ana', 'Ben'] },\n      { paidBy: 'Ana', amountCents: 1000, splitAmong: ['Ana', 'Ben'] },\n    ]);\n\n    expect(balances).toEqual({ Ana: 1500, Ben: -1500 });\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-payer-debited","suite":"payer-debited"},{"id":"catches-floored-shares","suite":"floored-shares"},{"id":"catches-overwrites-payer","suite":"overwrites-payer"}],"title":"من تغطية 100% إلى ثقة حقيقية","prompt":"<p>يشغّل الاختبار المبدئي كل سطر في <code dir=\"ltr\">computeBalances</code> (و<code dir=\"ltr\">splitEvenly</code>)، فسيُظهر تقرير الـ coverage نسبة 100%. ومع ذلك لا يفحص إلا أن ثلاثة أسماء عادت.</p>\n<p>قوِّ المجموعة حتى تلتقط ثلاثة أخطاء مخفيّة تفوت مجموعة الـ 100%: نسخة <strong>تخصم</strong> من الدافع بدل أن تضيف إليه، ونسخة <strong>تأخذ الجزء الصحيح</strong> من كل حصة فتختفي السنتات المتبقّية (فلا يعود مجموع الأرصدة صفرًا)، ونسخة <strong>تستبدل</strong> رصيد الدافع بدل أن تضيف إليه حين يدفع مرتين. القواعد مذكورة في تعليق الـ subject.</p>\n","hints":["استبدل الـ assertion الذي يعدّ المفاتيح بالنتيجة الدقيقة: Ana تدفع 3000 مقسّمة على ثلاثة، إذن <code dir=\"ltr\">{ Ana: 2000, Ben: -1000, Cai: -1000 }</code>.","استخدم فاتورة لا تنقسم بالتساوي، مثل 1000 على ثلاثة أشخاص، وأكّد أن مجموع الأرصدة يساوي 0.","أعطِ الدافع نفسه مصروفين وتحقّق من أن كليهما محسوب."],"explanation":"<p>ظلّت الـ coverage عند 100% طوال الوقت: لم تشغّل اختباراتك الجديدة أي سطر لم يشغّله الاختبار القديم. ما تغيّر هو <strong>ما تفحصه الاختبارات</strong>. يلتقط <code dir=\"ltr\">toEqual</code> الدقيق على المصروف الأول خطأ الإشارة. وفاتورة من 1000 سنت على ثلاثة أشخاص تترك سنتًا زائدًا، فتجعل الحصص المأخوذ جزؤها الصحيح مجموع الأرصدة +1 بدل 0؛ وثابت المجموع من القسم 2 يلتقط ذلك. ومصروفان من الدافع نفسه يشغّلان التراكم، وهو ما لا يستطيعه مصروف واحد أبدًا، مع أن سطر التراكم كان ‘مغطّى’ أصلًا. هذه هي الفجوة بين التنفيذ والتحقّق، وهي ما يقيسه الـ mutation testing مباشرة.</p>\n","checkLabels":{"passes":"كل اختباراتك تنجح على computeBalances الحقيقية","catches-payer-debited":"تلتقط النسخة التي تخصم من الدافع","catches-floored-shares":"تلتقط النسخة التي تضيّع السنتات المتبقّية","catches-overwrites-payer":"تلتقط النسخة التي تستبدل رصيد الدافع"}},"words":594,"readMinutes":3}