{"id":"l-tj-5-3","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تشرح كيف يقيّم الـ mutation testing مجموعة الاختبارات، وتشغّل StrykerJS مع Vitest على المنطق الأساسي، وتحوّل الـ mutants الناجية إلى الاختبارات المحدّدة التي كانت ناقصة.","html":"<p>ترك لك التمرين الأخير حقيقة مزعجة: قالت الـ coverage إن النسبة 100%، ولم تلتقط المجموعة شيئًا. تحتاج إلى قياس يطرح السؤال الحقيقي: <em>هل ستلاحظ هذه الاختبارات لو كان الكود خاطئًا؟</em> يجيب الـ mutation testing (اختبار الطفرات) عن هذا السؤال بالتجربة. وقد كنت تفعل ذلك يدويًا أصلًا: كل تمرين في هذه الدورة شغّل اختباراتك على “mutants” (نسخ طافرة). والأداة تفعل الشيء نفسه بشكل منهجي، لكل سطر.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">كيف يعمل</h2>\n<ol>\n<li>تصنع الأداة <strong>mutant</strong>: نسخة من كودك فيها تغيير صغير واحد، من النوع الذي يزلّ فيه الإنسان. يصبح <code dir=\"ltr\">&lt;</code> هو <code dir=\"ltr\">&lt;=</code>، و<code dir=\"ltr\">+</code> يصبح <code dir=\"ltr\">-</code>، و<code dir=\"ltr\">Math.min</code> يصبح <code dir=\"ltr\">Math.max</code>، ويصبح شرط ما <code dir=\"ltr\">true</code>، وتُفرَّغ كتلة كود.</li>\n<li>تشغّل الاختبارات التي تغطّي ذلك السطر على الـ mutant.</li>\n<li>إن فشل أيّ اختبار، <strong>يُقتَل</strong> الـ mutant: اختباراتك ستلتقط ذلك الخطأ. وإن نجحت كل الاختبارات، فقد <strong>نجا</strong>: ذلك الخطأ سيصل إلى المستخدمين.</li>\n</ol>\n<p>تكرّر هذا مع مئات أو آلاف الـ mutants. و<strong>مقياس الطفرات</strong> (mutation score) هو نسبة المقتول منها.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-tj-5-3-t1\"><defs><marker id=\"arrow-l-tj-5-3-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-3-t1\">يُنتج الكود المصدري ثلاثة mutants. تعمل مجموعة الاختبارات على كل منها: يفشل اختبار مع اثنين فيُقتلان، وتنجح كلها مع الثالث فينجو، مشيرًا إلى اختبار ناقص.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"85\" width=\"120\" height=\"60\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"70\" y=\"120\" text-anchor=\"middle\">settleUp</text>\n  <path class=\"d-arrow\" d=\"M130 105 L198 45\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M130 115 L198 115\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M130 125 L198 185\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"25\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"295\" y=\"50\" text-anchor=\"middle\">min → max</text>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"95\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"295\" y=\"120\" text-anchor=\"middle\">-cents → +cents</text>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"165\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"295\" y=\"190\" text-anchor=\"middle\">sum !== 0 → false</text>\n  <path class=\"d-arrow\" d=\"M390 45 L478 45\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M390 115 L478 115\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M390 185 L478 185\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"480\" y=\"25\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"575\" y=\"50\" text-anchor=\"middle\">فشل اختبار: قُتل</text>\n  <rect class=\"d-box-success\" x=\"480\" y=\"95\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"575\" y=\"120\" text-anchor=\"middle\">فشل اختبار: قُتل</text>\n  <rect class=\"d-box-warn\" x=\"480\" y=\"165\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"575\" y=\"190\" text-anchor=\"middle\">نجحت كلها: نجا</text>\n</svg><figcaption>كل mutant خطأ صغير متعمَّد واحد؛ فإمّا أن تقتله الاختبارات أو تدعه ينجو</figcaption></figure>\n<p>تسأل الـ coverage “هل عمل هذا السطر؟” ويسأل الـ mutation testing “لو كان هذا السطر خاطئًا، هل سيلاحظ أحد؟” والسؤال الثاني هو الذي يهمّك.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">تشغيل StrykerJS مع Vitest</h2>\n<p>StrykerJS هي أداة الـ mutation testing الراسخة لـ JavaScript وTypeScript، ولها plugin مشغّل لـ Vitest:</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\"> @stryker-mutator/core</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @stryker-mutator/vitest-runner</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> stryker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> init</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">   # interactive; or write the config yourself</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">stryker.config.json</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:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"$schema\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"./node_modules/@stryker-mutator/core/schema/stryker-schema.json\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"testRunner\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"vitest\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"plugins\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"@stryker-mutator/vitest-runner\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"mutate\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"src/money/**/*.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"!src/**/*.test.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"reporters\"</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 style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"clear-text\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"progress\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"thresholds\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"high\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">80</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"low\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">60</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"break\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">50</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"incremental\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>يشغّل <code dir=\"ltr\">npx stryker run</code> مجموعتك أولًا مرة واحدة ليعرف أيّ الاختبارات تغطّي أيّ الأسطر، ثم يختبر كل mutant. ويعرض تقرير HTML الكود المصدري مع تعليم كل mutant بأنه قُتل أو نجا. ويُفشل <code dir=\"ltr\">thresholds.break</code> التشغيل إن كان المقياس دونه، ويعيد <code dir=\"ltr\">incremental</code> استخدام النتائج للكود والاختبارات التي لم تتغيّر منذ التشغيل الأخير، وهذا مهم جدًا للسرعة.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">قراءة الناجين</h2>\n<p>الحالات التي ستراها أكثر من غيرها:</p>\n<ul>\n<li><strong>Killed (مقتول)</strong>: فشل اختبار. جيد.</li>\n<li><strong>Survived (ناجٍ)</strong>: نجح كل اختبار يغطّيه. assertion ناقص أو ضعيف.</li>\n<li><strong>No coverage (بلا تغطية)</strong>: لم يشغّل أيّ اختبار ذلك السطر أصلًا. وكانت الـ coverage ستخبرك بهذا أيضًا.</li>\n<li><strong>Timeout (انتهاء المهلة)</strong>: سبّب الـ mutant حلقة لا نهائية فانتهت مهلة الاختبارات. ويُحسب على أنه مكتشَف.</li>\n</ul>\n<p>الناجون هم الكنز. بالنسبة إلى <code dir=\"ltr\">settleUp</code>، قد يقول التقرير:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Text</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>[Survived] MethodExpression   src/money/settle-up.js:21\n-   const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n+   const amountCents = Math.max(debtors[d].cents, creditors[c].cents);\n\n[Survived] ConditionalExpression   src/money/settle-up.js:4\n-   if (sum !== 0) throw new Error('Balances must add up to zero');\n+   if (false) throw new Error('Balances must add up to zero');</code></pre></div>\n<p>كل واحد منها تعليمة دقيقة. الأول يقول إن لا اختبار يستخدم مبالغ يدين فيها المدين بمجموع مختلف عمّا يستحقّه الدائن، فيعطي <code dir=\"ltr\">min</code> و<code dir=\"ltr\">max</code> الإجابة نفسها في كل اختبار. والثاني يقول إن لا اختبار يمرّر مدخلات غير متوازنة أبدًا. اكتب هذين الاختبارين فيُقتل الاثنان معًا.</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>مطاردة مقياس طفرات 100%</span></p>\n<p>بعض الـ mutants <strong>متكافئة</strong> (equivalent): التغيير لا يبدّل أي سلوك يستطيع مستدعٍ ملاحظته. قلب اتجاه الترتيب في <code dir=\"ltr\">settleUp</code> كثيرًا ما يظلّ يُنتج تسوية صالحة؛ والـ mutant الذي يغيّر <code dir=\"ltr\">cents &lt; 0</code> إلى <code dir=\"ltr\">cents &lt;= 0</code> قد لا يؤثّر إلا في من رصيدهم صفر، والحلقة لا تصل إليهم أبدًا. كتابة اختبارات لقتل هذه تعني تأكيد تفاصيل داخلية. اقبلها، أو عطّل تلك الطفرة بعينها على ذلك السطر بتعليق مثل <code dir=\"ltr\">// Stryker disable next-line all: equivalent, order doesn't matter</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">الـ mutants المنتقاة يدويًا مقابل المولّدة</h2>\n<p>الـ mutants في تمارين هذه الدورة انتُقيت يدويًا: كل واحد منها يحاكي خطأً يكتبه الناس فعلًا، مثل معامل متبادل الموقع أو <code dir=\"ltr\">await</code> منسيّة. أما mutants الـ Stryker فتُولَّد بشكل منهجي من مجموعة ثابتة من المحوِّرات (mutators)، عادةً عشرات منها لكل دالة، فتجد فجوات لم تكن لتخطر لك، وتُنتج أيضًا بعض الضجيج. كلاهما مفيد. حين تراجع اختبارات زميل، فعادة الانتقاء اليدوي هي النسخة السريعة: اسأل “ما الخطأ الأرجح هنا، وأيّ اختبار سيفشل؟” وحين تريد قياسًا على مستوى وحدة كاملة، دع الأداة تولّد القائمة.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">أين يناسب</h2>\n<p>الـ mutation testing بطيء: مئات الـ mutants، كل منها يشغّل جزءًا من مجموعتك. بضع عادات تُبقيه عمليًا:</p>\n<ul>\n<li><strong>احصره</strong> بـ <code dir=\"ltr\">mutate</code> في الكود الذي تكون أخطاؤه مكلفة: المال، والصلاحيات، والتحليل. لا في كود ربط الواجهة.</li>\n<li><strong>شغّله تدريجيًا</strong> محليًا، ووفق جدول زمني (ليليًا) أو على الملفات المتغيّرة في CI، بدل تشغيله على المستودع كله مع كل push.</li>\n<li><strong>استخدم التقرير في المراجعة</strong>: الـ pull request الذي يضيف منطقًا وmutants ناجية يستحق سؤالًا.</li>\n</ul>\n<p>في التمرين تحصل على مجموعة ضعيفة لـ <code dir=\"ltr\">settleUp</code> وقائمة بالناجين، وتكتب الاختبارات التي تقتلهم.</p>\n","toc":[{"id":"sec-1","text":"كيف يعمل","level":2},{"id":"sec-2","text":"تشغيل StrykerJS مع Vitest","level":2},{"id":"sec-3","text":"قراءة الناجين","level":2},{"id":"sec-4","text":"الـ mutants المنتقاة يدويًا مقابل المولّدة","level":2},{"id":"sec-5","text":"أين يناسب","level":2}],"takeaways":["تصنع أداة الـ mutation testing (اختبار الطفرات) أخطاءً صغيرة متعمَّدة (mutants) في كودك وتعيد تشغيل الاختبارات؛ والـ mutant الذي يُفشل اختبارًا يُقتَل (killed)، والذي لا يُفشل أيّ اختبار ينجو (survived).","مقياس الطفرات (mutation score)، أي نسبة الـ mutants المقتولة من مجموعها، يقيس كم من الأخطاء المعقولة ستلتقطها اختباراتك، وهذا ما لا تستطيعه الـ coverage.","كل mutant ناجٍ يشير إلى assertion ناقص أو ضعيف؛ اقرأه، ثم اكتب الاختبار الذي يقتله.","بعض الـ mutants متكافئة (لا تغيّر السلوك) ولا يمكن قتلها؛ تجاهلها بدل ليّ الاختبارات.","الـ mutation testing بطيء، فشغّله على المنطق الأساسي، بشكل تدريجي أو وفق جدول زمني، لا على كل ملف في كل commit."],"further":[{"title":"Vitest — Coverage guide","url":"https://vitest.dev/guide/coverage"},{"title":"StrykerJS on GitHub","url":"https://github.com/stryker-mutator/stryker-js"}],"quiz":[{"q":"<p>يبلّغ Stryker بما يلي: <code dir=\"ltr\">ConditionalExpression: if (sum !== 0) → if (false)</code> — Survived. ماذا يخبرك ذلك؟</p>","options":[{"text":"الفحص غير ضروري ويمكن حذفه.","why":"يخبرك أن لا اختبار يلاحظ حين يُزال الفحص، لا أن الفحص عديم الفائدة؛ فالمدخلات غير المتوازنة ستُنتج تحويلات خاطئة بصمت."},{"text":"لا يوجد اختبار يمرّر أرصدة غير متوازنة ويتوقّع أن ترمي <code dir=\"ltr\">settleUp</code> خطأً.","why":"صحيح. أضف اختبارًا مثل <code dir=\"ltr\">expect(() =&gt; settleUp({ Ana: 500, Ben: -400 })).toThrow()</code> فيُقتل الـ mutant."},{"text":"فشل Stryker في تشغيل الاختبارات لذلك السطر.","why":"حين لا تستطيع الاختبارات العمل، تكون الحالة خطأ تشغيل أو ترجمة، أو “no coverage”؛ أما “Survived” فتعني أن الاختبارات عملت ونجحت كلها."}],"answer":1},{"q":"<p>تغطية الأسطر في مجموعتك 100% ومقياس الطفرات 55%. أيّ تلخيص هو الأفضل؟</p>","options":[{"text":"كل سطر يعمل، لكن قرابة نصف الأخطاء المعقولة التي أدخلها Stryker ستمرّ دون أن يلاحظها أحد.","why":"صحيح. الـ coverage تُظهر التنفيذ؛ ومقياس الطفرات يُظهر كم من ذلك التنفيذ يُفحص فعلًا."},{"text":"المجموعة ممتازة؛ فمقاييس الطفرات فوق 50% نادرة.","why":"لا يوجد معيار عالمي، لكن نجاة 45% من الـ mutants في المنطق الأساسي تعني عادةً assertions ضعيفة تستحق الإصلاح."},{"text":"الرقمان متناقضان، فإحدى الأداتين مضبوطة بشكل خاطئ.","why":"يقيسان شيئين مختلفين؛ والتغطية العالية مع مقياس طفرات منخفض هو النمط الكلاسيكي للاختبارات الشحيحة بالـ assertions."}],"answer":0},{"q":"<p>يغيّر mutant التعبير <code dir=\"ltr\">b.cents - a.cents</code> إلى <code dir=\"ltr\">a.cents - b.cents</code> في دالة المقارنة الخاصة بالترتيب، وكل مدخل صالح ما زال يُنتج تسوية صحيحة. ماذا يجب أن تفعل؟</p>","options":[{"text":"اكتب اختبارًا يؤكّد الترتيب الدقيق للتحويلات في مجموعة كبيرة، كي يموت الـ mutant.","why":"إن كانت عدة ترتيبات صالحة، فتثبيت واحد منها يربط الاختبار بالتفاصيل الداخلية، وهو الخطأ نفسه من القسم 2."},{"text":"اخفض الحدّ الأدنى لمقياس الطفرات إلى الصفر.","why":"mutant متكافئ واحد ليس سببًا للتخلّص من الإشارة التي تأتي من كل الـ mutants الأخرى."},{"text":"تعامل معه على أنه mutant متكافئ على الأرجح وتجاهله (أو عطّل تلك الطفرة على ذلك السطر بتعليق).","why":"صحيح. إن لم يكن بإمكان أي مستدعٍ ملاحظة الفرق، فلا يوجد ما يُختبر؛ ويتيح لك Stryker تعليم هذه الأسطر كي يبقى التقرير ذا معنى."}],"answer":2}],"exercise":{"kind":"web","mode":"test","subject":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n","mutants":[{"id":"min-to-max","code":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.max(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n"},{"id":"no-balance-check","code":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (false) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n"},{"id":"unary-sign","code":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: +cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n"},{"id":"swapped-direction","code":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: creditors[c].name, to: debtors[d].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n"}],"starter":{"js":"// Stryker's report on this suite:\n//   [Survived] MethodExpression       Math.min(...) -> Math.max(...)\n//   [Survived] ConditionalExpression  if (sum !== 0) -> if (false)\n//   [Survived] UnaryOperator          cents: -cents -> cents: +cents\n// and a bug from last month's code review:\n//   [Survived] from/to swapped in each transfer\ndescribe('settleUp', () => {\n  test('returns no transfers when everyone is settled', () => {\n    expect(settleUp({ Ana: 0, Ben: 0 })).toEqual([]);\n  });\n\n  test('returns one transfer for two people', () => {\n    expect(settleUp({ Ana: 500, Ben: -500 })).toHaveLength(1);\n  });\n});\n"},"solution":{"js":"const sum = (xs) => xs.reduce((a, b) => a + b, 0);\n\ndescribe('settleUp', () => {\n  test('returns no transfers when everyone is settled', () => {\n    expect(settleUp({ Ana: 0, Ben: 0 })).toEqual([]);\n  });\n\n  test('the debtor pays the creditor the full amount', () => {\n    expect(settleUp({ Ana: 500, Ben: -500 })).toEqual([\n      { from: 'Ben', to: 'Ana', amountCents: 500 },\n    ]);\n  });\n\n  test('one debtor pays two creditors exactly what each is owed', () => {\n    const transfers = settleUp({ Ana: -1000, Ben: 600, Cai: 400 });\n\n    expect(transfers).toEqual([\n      { from: 'Ana', to: 'Ben', amountCents: 600 },\n      { from: 'Ana', to: 'Cai', amountCents: 400 },\n    ]);\n    expect(sum(transfers.map((t) => t.amountCents))).toBe(1000);\n  });\n\n  test('refuses balances that do not add up to zero', () => {\n    expect(() => settleUp({ Ana: 500, Ben: -400 })).toThrow('add up to zero');\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-min-to-max","suite":"min-to-max"},{"id":"catches-no-balance-check","suite":"no-balance-check"},{"id":"catches-unary-sign","suite":"unary-sign"},{"id":"catches-swapped-direction","suite":"swapped-direction"}],"title":"اقتل الـ mutants الناجية","prompt":"<p>تنجح مجموعة <code dir=\"ltr\">settleUp</code> هذه، لكن تقرير Stryker (المنسوخ في تعليق الكود المبدئي) يسرد ثلاثة mutants <strong>نجت</strong> منها: <code dir=\"ltr\">Math.min</code> تغيّر إلى <code dir=\"ltr\">Math.max</code>، وفحص المجموع الصفري معطّل، وإشارة السالب أُسقطت عند تسجيل ما يدين به المدين. ووجدت مراجعة الكود في الشهر الماضي خطأً رابعًا تفوته المجموعة أيضًا: <code dir=\"ltr\">from</code> و<code dir=\"ltr\">to</code> تبادلا موقعيهما في كل تحويل.</p>\n<p>اكتب الاختبارات التي تقتل الأربعة كلها، دون أن تكسر المجموعة على الكود الحقيقي. الـ subject هو <code dir=\"ltr\">settleUp</code> التي بنيتها في درس الـ TDD.</p>\n","hints":["<code dir=\"ltr\">toHaveLength(1)</code> لا يستطيع التمييز بين تحويل صحيح وآخر خاطئ. أكّد التحويل كله بـ <code dir=\"ltr\">toEqual([{ from: 'Ben', to: 'Ana', amountCents: 500 }])</code>؛ فهذا يقتل اثنين من الـ mutants دفعة واحدة.","يتّفق <code dir=\"ltr\">Math.min</code> و<code dir=\"ltr\">Math.max</code> كلما كان المدين مدينًا بالضبط بما يستحقّه الدائن. استخدم مدينًا واحدًا ودائنَين بمبلغين مختلفين، مثل <code dir=\"ltr\">{ Ana: -1000, Ben: 600, Cai: 400 }</code>.","للفحص المعطّل، مرّر أرصدة لا يساوي مجموعها صفرًا وتوقّع رمي خطأ: <code dir=\"ltr\">expect(() =&gt; settleUp({ Ana: 500, Ben: -400 })).toThrow()</code>."],"explanation":"<p>كل ناجٍ يقابله assertion ناقص واحد. نجا الاتجاه المتبادل وإشارة السالب المُسقطة كلاهما لأن <code dir=\"ltr\">toHaveLength(1)</code> لم ينظر داخل التحويل قط؛ وتأكيد التحويل بالضبط يقتلهما معًا (أحدهما يُنتج دفع Ana لـ Ben، والآخر تحويلًا بقيمة -500). ونجا <code dir=\"ltr\">Math.max</code> لأن المدين في كل اختبار كان يدين بالضبط بما للدائن، فيتّفق <code dir=\"ltr\">min</code> و<code dir=\"ltr\">max</code>؛ أما مع مدين واحد عليه 1000 ودائنَين لهما 600 و400، فإن <code dir=\"ltr\">max</code> يدفع لـ Ben مبلغ 1000 ثم يتوقّف، فتفشل القائمة الدقيقة. ونجا فحص المجموع الصفري لأن لا اختبار أرسل مدخلات غير متوازنة قط. ولاحظ ما <strong>لم</strong> تحتج إليه: اختبار لترتيب التحويلات في المجموعات الكبيرة. فذلك الـ mutant سيكون متكافئًا في حالات كثيرة، وتثبيته سيختبر التفاصيل الداخلية لا السلوك.</p>\n","checkLabels":{"passes":"كل اختباراتك تنجح على settleUp الحقيقية","catches-min-to-max":"تقتل الـ mutant الذي يحوّل Math.min إلى Math.max","catches-no-balance-check":"تقتل الـ mutant الذي عُطّل فيه فحص المجموع الصفري","catches-unary-sign":"تقتل الـ mutant الذي يُسقط إشارة السالب للمدينين","catches-swapped-direction":"تقتل الـ mutant الذي يبادل بين from وto"}},"words":624,"readMinutes":4}