مولّدات الاختبارات


مولّد الاختبارات هو أداة برمجية خاصة بمسار معيّن، وظيفتها توليد اختبارات تمرين تطبيقي تلقائيًا. وهي تفعل ذلك بتحويل حالات اختبار التمرين بصيغة JSON إلى اختبارات بلغة المسار.

المزايا

من مزايا وجود مولّد اختبارات:

  1. يمكن إضافة التمارين بسرعة أكبر
  2. يؤتمت الأجزاء «المملة» من إضافة تمرين
  3. يسهّل مزامنة الاختبارات مع أحدث البيانات القياسية

حالات الاستخدام

عمومًا، يُشغّل المرء مولّد الاختبارات لأحد غرضين:

  1. توليد اختبارات تمرين جديد
  2. تحديث اختبارات تمرين قائم

توليد اختبارات تمرين جديد

إضافة مولّد اختبارات لتمرين جديد تتيح لك توليد ملف (أو ملفات) اختباراته. وبافتراض أن مولّد الاختبارات نفسه قد نُفّذ بالفعل، فإن توليد اختبارات التمرين الجديد سيكون عملًا أقل بكثير من كتابتها من الصفر.

تحديث اختبارات تمرين قائم

بمجرد أن يصبح لتمرين ما مولّد اختبارات، يمكنك تشغيله من جديد لتحديث التمرين ومزامنته مع أحدث بياناته القياسية. نوصي بالقيام بذلك بشكل دوري، للتحقق مما إذا كانت هناك حالات اختبار إشكالية تحتاج إلى تحديث، أو اختبارات جديدة قد ترغب في إضافتها.

نقطة البداية

هناك نقطتان محتملتان للبداية عند تنفيذ مولّد اختبارات لتمرين:

  1. التمرين جديد، وبالتالي ليس له أي اختبارات
  2. التمرين موجود بالفعل، وبالتالي له اختبارات قائمة
Caution

إذا كانت هناك اختبارات قائمة، فنفّذ مولّد الاختبارات بحيث لا تُعطّل الاختبارات التي يولّدها الحلول الموجودة.

التصميم

بوجه عام، يعتمد توليد ملفات الاختبار على أحد أمرين:

  • الكود: تُولَّد ملفات الاختبار (في معظمها) عبر الكود
  • القوالب: تُولَّد ملفات الاختبار (في معظمها) باستخدام القوالب

اتضح لنا أن النهج القائم على الكود يؤدي إلى كود مولّد اختبارات معقّد نسبيًا، في حين أن النهج القائم على القوالب أبسط.

وما نوصي به هو التسلسل التالي:

  1. اقرأ البيانات القياسية للتمرين
  2. استبعد حالات الاختبار المعلَّمة بـ include = false في ملف tests.toml الخاص بالتمرين
  3. حوّل البيانات القياسية للتمرين إلى صيغة يمكن استخدامها في قالب
  4. مرّر البيانات القياسية للتمرين إلى قالب خاص بالتمرين

الميزة الأساسية لهذا الترتيب هي أن لكل تمرين قالبه الخاص، وهذا:

  • يجعل كيفية توليد ملفات الاختبار واضحة
  • يجعل تصحيح الأخطاء فيها أسهل
  • يجعل تعديلها آمنًا دون خطر تعطيل تمرين آخر
Caution

عند تصميم مولّد الاختبارات، حاول أن:

  • تقلّل المعالجة المسبقة للبيانات القياسية داخل مولّد الاختبارات إلى الحد الأدنى
  • تقلّل الترابط بين القوالب

التنفيذ

عادةً ما يُكتب مولّد الاختبارات (في معظمه) بلغة المسار.

Caution

يمكنك بحرية استخدام لغات أخرى، لكن كل لغة إضافية ستجعل صيانة المسار أو المساهمة فيه أصعب. لذلك نوصي باستخدام لغة المسار حيث أمكن، لأن ذلك يسهّل الصيانة والمساهمة.

التنسيق

إذا كان لمسارك أدوات لتنسيق الكود، ففكّر في تشغيلها كخطوة معالجة لاحقة بعد تصيير قالبك.

البيانات القياسية

البيانات الأساسية التي يعمل عليها مولّد الاختبارات هي ملف canonical-data.json الخاص بالتمرين. يُعرَّف هذا الملف في مستودع exercism/problem-specifications، الذي يحدد بيانات وصفية مشتركة لكثير من تمارين Exercism.

Caution

ليست كل التمارين لديها ملف canonical-data.json! وإذا لم يكن لديها، فستحتاج إلى إنشاء الاختبارات يدويًا، إذ لا توجد بيانات ليعمل عليها مولّد الاختبارات.

البنية

تُعرَّف البيانات القياسية في كائن JSON. يحتوي هذا الكائن على حقل "cases" يضم حالات الاختبار. وهذه الحالات (عادةً) تقابل اختبارات في مسارك واحدًا بواحد.

لكل حالة اختبار عدة خصائص، وأهمها الوصف، والخاصية، وقيم المدخلات، والقيمة المتوقعة. إليك مثالًا (جزئيًا) على ملف canonical-data.json الخاص بتمرين leap:

{
  "exercise": "leap",
  "cases": [
    {
      "uuid": "6466b30d-519c-438e-935d-388224ab5223",
      "description": "year not divisible by 4 in common year",
      "property": "leapYear",
      "input": {
        "year": 2015
      },
      "expected": false
    },
    {
      "uuid": "4fe9b84c-8e65-489e-970b-856d60b8b78e",
      "description": "year divisible by 4, not divisible by 100 in leap year",
      "property": "leapYear",
      "input": {
        "year": 1996
      },
      "expected": true
    }
  ]
}

تتمثل المسؤولية الأساسية لمولّد الاختبارات في تحويل بيانات JSON هذه إلى اختبارات خاصة بالمسار. وإليك كيف يمكن ترجمة JSON أعلاه إلى كود اختبار بلغة Nim:

import unittest
import leap

suite "Leap":
  test "year not divisible by 4 in common year":
    check isLeapYear(2015) == false

  test "year divisible by 4, not divisible by 100 in leap year":
    check isLeapYear(1996) == true

بنية ملف canonical-data.json موثّقة جيدًا، وله أيضًا تعريف مخطط JSON.

التداخل

تستخدم بعض التمارين التداخل في بياناتها القياسية. هذا يعني أن كل عنصر في مصفوفة cases يمكن أن يكون:

  1. حالة اختبار عادية (بلا حالات اختبار فرعية)
  2. مجموعة من حالات الاختبار (حالة فرعية واحدة أو أكثر)
Note

يمكنك تمييز نوع العنصر بالتحقق من وجود حقول خاصة بنوع واحد من العناصر. وأفضل وسيلة لذلك على الأرجح هي استخدام مفتاح "cases"، فهو لا يظهر إلا في مجموعات حالات الاختبار.

إليك مثالًا على حالات اختبار متداخلة:

{
  "cases": [
    {
      "uuid": "e9c93a78-c536-4750-a336-94583d23fafa",
      "description": "data is retained",
      "property": "data",
      "input": {
        "treeData": ["4"]
      },
      "expected": {
        "data": "4",
        "left": null,
        "right": null
      }
    },
    {
      "description": "insert data at proper node",
      "cases": [
        {
          "uuid": "7a95c9e8-69f6-476a-b0c4-4170cb3f7c91",
          "description": "smaller number at left node",
          "property": "data",
          "input": {
            "treeData": ["4", "2"]
          },
          "expected": {
            "data": "4",
            "left": {
              "data": "2",
              "left": null,
              "right": null
            },
            "right": null
          }
        }
      ]
    }
  ]
}
Caution

إذا كان مسارك لا يدعم تجميع الاختبارات، فستحتاج إلى:

  • اجتياز التسلسل الهرمي لـ cases أو تسويته لتحصل في النهاية على حالات الاختبار الأعمق (الأوراق) فقط
  • دمج وصف حالة الاختبار مع أوصاف الحالات الأصلية لإنشاء اسم اختبار فريد

قيم المدخلات والقيم المتوقعة

يختلف محتوى مفتاحي input وexpected في حالة الاختبار اختلافًا واسعًا. وفي معظم الحالات تكون قيمًا مفردة (مثل الأعداد أو القيم المنطقية أو السلاسل النصية) أو كائنات بسيطة. لكنك قد تجد أحيانًا قيمًا أكثر تعقيدًا تحتاج على الأرجح إلى بعض المعالجة المسبقة، مثل دوال لامبدا في الكود الزائف، أو قوائم من العمليات التي تُنفَّذ على كود الطالب، وغير ذلك.

السيناريوهات

لحالات الاختبار حقل اختياري هو scenarios. ويمكن لمولّد الاختبارات استخدام هذا الحقل لمعالجة حالات اختبار معيّنة معالجة خاصة. وأكثر استخدام شيوعًا هو تجاهل أنواع معيّنة من الاختبارات، كالاختبارات ذات السيناريو "unicode"، إذ قد لا تدعم لغة مسارك Unicode.

تجد القائمة الكاملة للسيناريوهات هنا.

قراءة ملفات canonical-data.json

هناك عدة خيارات لقراءة ملفات canonical-data.json:

  1. جلبها مباشرة من مستودع problem-specifications (مثل https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json).
  2. إضافة مستودع problem-specifications كوحدة فرعية Git إلى مستودع المسار.
  3. قراءتها من ذاكرة configlet المؤقتة. ويتوقف الموقع على نظام المستخدم، لكن يمكنك استخدام configlet info -o -v d | head -1 | cut -d " " -f 5 للحصول على الموقع برمجيًا.

حالات اختبار خاصة بالمسار

إذا أراد مسارك إضافة بعض حالات الاختبار الإضافية الخاصة به (غير الموجودة في البيانات القياسية)، فأحد الخيارات هو إنشاء ملف additional-test-cases.json، يمكن لمولّد الاختبارات حينها دمجه مع ملف canonical-data.json قبل تمريره إلى القالب للتصيير.

القوالب

محرّك القوالب المستخدم سيكون على الأرجح خاصًا بالمسار. ومن المثالي أن تكون قوالبك مباشرة قدر الإمكان، فلا تقلق بشأن تكرار الكود وما شابه.

تحصل القوالب نفسها على بياناتها من مولّد الاختبارات، الذي يمرّ على هذه البيانات لتصيير القوالب.

Note

للمساعدة في إبقاء القوالب بسيطة، قد يكون من المفيد إجراء قليل من المعالجة المسبقة على جانب مولّد الاختبارات، أو تعريف بعض «المرشّحات» أو أي آلية توسعة تسمح بها قوالبك.

استخدام configlet

configlet هي الأداة الأساسية لصيانة المسار، ويمكن استخدامها من أجل:

  • إنشاء ملفات تمرين جديد: شغّل bin/configlet create --practice-exercise <slug>
  • مزامنة ملف tests.toml لتمرين قائم: شغّل bin/configlet sync --tests --update --exercise <slug>
  • جلب البيانات القياسية للتمرين إلى القرص (وهذا أثر جانبي لأي من الأمرين أعلاه)

وهذا يجعل من configlet أداة رائعة للاستخدام مع مولّد الاختبارات في سير عمل قوي حقًا.

واجهة سطر الأوامر

ستريد أن يكون استخدام مولّد الاختبارات سهلًا _و_قويًا في آن واحد. ولذلك نوصي بإنشاء ملف سكربت أو أكثر.

Note

لك حرية اختيار صيغة ملف السكربت الأنسب لمسارك. وسكربتات Shell وسكربتات PowerShell خياران شائعان وكلاهما يعمل جيدًا.

إليك مثالًا على سكربت shell يجمع بين configlet ومولّد اختبارات لإنشاء هيكل تمرين جديد بسرعة:

bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>

البناء من الصفر

قبل أن تبدأ في بناء مولّد اختبارات، نقترح أن تطّلع على بضعة مولّدات اختبارات موجودة لتأخذ فكرة عن كيفية تنفيذ المسارات الأخرى لها:

إذا كانت لديك أي أسئلة، فإن المنتدى هو أفضل مكان لطرحها. وقد تفيدك أيضًا نقاشات المنتدى حول مولّد اختبارات Rust ومولّد اختبارات JavaScript.

الحد الأدنى من المنتج القابل للاستخدام

نوصي ببناء مولّد الاختبارات تدريجيًا، بدءًا من الحد الأدنى من المنتج القابل للاستخدام. وأبسط نسخة ممكنة ستقرأ ملف canonical-data.json الخاص بالتمرين وتمرّر تلك البيانات إلى القالب فقط.

ابدأ بالتركيز على تمرين واحد، ويُفضّل أن يكون بسيطًا مثل leap. ولا تبدأ في إضافة المزيد من التمارين تدريجيًا إلا بعد أن يعمل ذلك بنجاح.

وحاول أن تُبقي مولّد الاختبارات بسيطًا قدر الإمكان.

Note

من المثالي أن يتمكن المساهم من لصق قالب موجود أو تعديله دون الحاجة إلى فهم كيفية عمل مولّد الاختبارات من الداخل.

الاستخدام أو المساهمة

تختلف كيفية استخدام مولّد الاختبارات أو المساهمة فيه من مسار لآخر. ابحث عن التعليمات في ملف README.md أو CONTRIBUTING.md الخاص بالمسار، أو في مجلد كود مولّد الاختبارات.