الوحدة 2 · اختبارات وحدة لها قيمة
الحالات الحدّية والاختبارات المعتمدة على الجداول
20 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تجد المدخلات التي تكشف الأخطاء باستخدام فئات التكافؤ والقيم الحدّية، ثم تغطّيها بإيجاز باختبارات معتمدة على الجداول عبر test.each.
- 1الدالة الخاضعة للاختبار
- 2فئات التكافؤ والحدود
- 3الاختبارات المعتمدة على الجداول باستخدام test.each
- 4من أين تأتي الحالات الحدّية
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
معظم الأخطاء لا تعيش في منتصف نطاق المدخلات. لا ينكسر كود أحد مع “$12.34”. بل ينكسر مع “”، ومع “0”، ومع “12.5” حين تُقرأ اثني عشر دولارًا وخمسة سنتات، ومع " 7.10 " حين تُلصق بمسافة في آخرها. إيجاد هذه المدخلات عن قصد هو المهارة التي يعلّمها هذا الدرس.
الدالة الخاضعة للاختبار
يقبل حقل المبلغ في Splitwise-lite ما يكتبه الناس ويحوّله إلى سنتات صحيحة:
function parseAmount(input) {
const text = String(input).trim();
if (!/^\d+(\.\d{1,2})?$/.test(text)) {
throw new Error(`Not a valid amount: "${input}"`);
}
const [whole, fraction = ''] = text.split('.');
const cents = Number(whole) * 100 + Number(fraction.padEnd(2, '0'));
if (cents === 0) throw new Error('Amount must be greater than zero');
return cents;
}
for (const input of ['12', '12.5', ' 7.10 ', '0.01']) {
console.log(JSON.stringify(input), '->', parseAmount(input));
}
for (const input of ['', '0', '-5', '12.345', 'ten']) {
try { parseAmount(input); } catch (e) { console.log(JSON.stringify(input), '->', e.message); }
}فئات التكافؤ والحدود
لا تستطيع اختبار كل نص ممكن، ولا تحتاج إلى ذلك. تقسيم التكافؤ (equivalence partitioning) يعني جمع المدخلات التي يُفترض أن يعاملها الكود بالطريقة نفسها، واختبار ممثّل واحد من كل مجموعة:
- أعداد صحيحة:
'12' - منزلة عشرية واحدة:
'12.5'(وهي الخطرة: هل هي 1250 أم 1205؟) - منزلتان عشريتان:
'12.50' - محاطة بمسافات:
' 7.10 ' - نص غير صالح:
'ten'،''،'-5'
ثم يضيف تحليل القيم الحدّية (boundary value analysis) المدخلات الواقعة على الحواف بين المجموعات، لأن هناك بالذات يختلط < بـ <=، ويتحوّل {1,2} إلى {1,3}. لكل حدّ، اختبر ما تحته مباشرة، وعليه، وما فوقه مباشرة:
الحدود هنا هي الصفر (مرفوض) مقابل سنت واحد (مقبول)، ومنزلتان عشريتان (مقبول) مقابل ثلاث (مرفوض). وإليك قائمة أمرّ عليها مع أي مدخل:
- الفارغ والمسافات فقط:
''،' ' - صفر، وواحد، وكثير:
'0'،'0.01'،'12345.67' - كل حدّ، من الجهتين: منزلتان مقابل ثلاث
- أنواع خاطئة: حروف، وإشارة سالب، وفاصلة (
'1,000') - ضجيج التنسيق: مسافات في البداية أو النهاية، ونقطة في الآخر (
'12.')
الاختبارات المعتمدة على الجداول باستخدام test.each
كتابة test(...) منفصل لكل واحد من هذه المدخلات تزحم الملف. يأخذ test.each جدولًا وينتج اختبارًا مسمّى لكل صف:
import { describe, test, expect } from 'vitest';
import { parseAmount } from './parse-amount.js';
describe('parseAmount', () => {
test.each([
['12', 1200],
['12.5', 1250],
['12.50', 1250],
['0.01', 1],
[' 7.10 ', 710],
])('parses %s as %i cents', (input, cents) => {
expect(parseAmount(input)).toBe(cents);
});
test.each(['', '0', '0.00', '-5', '12.505', 'ten', '1,000'])(
'rejects %s',
(input) => {
expect(() => parseAmount(input)).toThrow();
},
);
});يُملأ %s و%i من كل صف، فيقرأ التقرير parses 12.5 as 1250 cents، ويسمّي الفشل المدخل بالضبط. وفي الجداول الأعرض، يمكن أن تكون الصفوف كائنات، ويمكن أن يستخدم الاسم $field:
test.each([
{ input: '12.5', cents: 1250 },
{ input: '0.01', cents: 1 },
])('parses $input as $cents cents', ({ input, cents }) => {
expect(parseAmount(input)).toBe(cents);
});المكسب الحقيقي في الصيانة. حين يبلّغ مستخدم أن '12.' يعطّل النموذج، يكون اختبار الانتكاس (regression test) صفًّا جديدًا واحدًا، لا كتلة كود جديدة.
من أين تأتي الحالات الحدّية
لا تجد الحالات الحدّية (edge cases) بالتحديق في الكود. ثلاثة مصادر تقوم بمعظم العمل:
- المواصفات، مقروءةً بعين الخصم. “حتى منزلتين عشريتين” تقترح فورًا منزلتين وثلاثًا. “يجب أن يكون موجبًا” تقترح الصفر. كل حدّ في المتطلّبات حدّ يُختبر من جهتيه.
- المستخدمون الحقيقيون. يلصق الناس المبالغ من كشوف الحساب البنكية مع مسافات ورموز عملات. ويكتب الناس في ألمانيا الفاصلة كفاصل عشري، فـ
'12,50'ليست خطأً مطبعيًا بالنسبة إليهم؛ قرّر إن كنت سترفضها برسالة مفيدة أو تقبلها، واكتب الصف في الحالتين. - تقارير الأخطاء. كل خطأ وصل إلى بيئة الإنتاج حالة حدّية فاتها جدولك. أضف الصف أولًا، وشاهده يفشل، ثم أصلح الكود. هكذا يصبح الجدول سجلًّا لكل ما سار على نحو خاطئ في أي وقت.
كم صفًّا يكفي؟ صف لكل فئة، وجهتا كل حدّ، وصف لكل خطأ سابق. بالنسبة إلى parseAmount هذا نحو خمسة عشر صفًّا، وبضعة أجزاء من الثانية وقت تشغيل، ونوم هانئ جدًا. وإن وجدت نفسك تريد آلاف المدخلات العشوائية، فانظر في مكتبات الاختبار القائم على الخصائص (property-based testing) مثل fast-check، التي تولّد المدخلات وتقلّص حالات الفشل إلى أصغر مثال؛ لكن في معظم أكواد التطبيقات، يكون الجدول المنتقى يدويًا أوضح.
في التمرين، ابنِ جدولًا يكشف أربعة أخطاء تحليل واقعية. وبعده: دوال ترمي أخطاءً ودوال تعيد promises.
أهم النقاط
- اجمع المدخلات في فئات يُفترض أن يعاملها الكود بالطريقة نفسها، ثم اختبر قيمة نموذجية واحدة من كل فئة، إضافة إلى الحواف بين الفئات.
- تتجمّع الأخطاء عند الحدود، لذا اختبر ما تحت كل حدّ مباشرة، وعليه، وما فوقه مباشرة (الصفر، والطول الأقصى، وحدّ المنازل العشرية).
- يحوّل
test.eachجدولًا من المدخلات والمخرجات المتوقّعة إلى اختبار مسمّى لكل صف، فتصبح إضافة حالة جديدة سطرًا واحدًا. - سمِّ كل صف باستخدام عناصر نائبة مثل
%sأو$fieldكي يخبرك الفشل أيّ مدخل انكسر.