التعامل الآمن مع تطبيق أعمال قديم بلا اختبارات ── الممارسة العمليّة لاختبار التوصيف وإعادة الهيكلة
· آخر تحديث: · غو كومورا · التقنيات القديمة, الاستفادة من الأصول القائمة, إعادة الهيكلة, تصميم الاختبارات, اختبار التوصيف, C#, .NET, الصيانة, جدول القرار, الاستشارات التقنية
«أعرف الموضع الذي أريد إصلاحه. لكن حين أفكّر أنّ لمسي له قد يكسر موضعًا آخر، لا أستطيع أن أمدّ يدي إليه» ── هذه عبارة نسمعها كثيرًا ممّن ورثوا تطبيق أعمال بلا اختبارات.
كثير من تطبيقات الأعمال المكتوبة بـVB6 أو .NET Framework أو Access لا تملك اختبارات آليّة. كما توقّف تحديث وثائق المواصفات أيضًا، فتصبح الحالة «الكود هو وثيقة المواصفات الوحيدة». ومع ذلك يستمرّ العمل، ولا تنتظر طلبات التعديل من نوع تغيير نسبة ضريبة الاستهلاك، وتصحيح تخطيط التقرير، وإضافة طرف تجاريّ.
تناولنا في مدوّنتنا في مقال «إلى متى ستستمرّ تطبيقات VB6 في العمل ── وضع دعم بيئة التشغيل والمسار العمليّ للترحيل إلى .NET» فكرة إجراء الترحيل من خلال مطابقة مخرَجات النظام القديم باعتباره «وثيقة مواصفات عاملة». يطبّق هذا المقال الفكرة نفسها على موقف ليس الترحيل، بل التعديل الفوريّ على الكود العامل حاليًّا في مكانه. الأداة المحوريّة هي اختبار التوصيف (characterization test). حتّى الكود الخالي من الاختبارات، إذا ثُبِّت السلوك الذي قد نكسره لاحقًا بالاختبار أوّلًا، تصبح إعادة الهيكلة وإضافة الميزات أكثر أمانًا بمراحل.
1. الخلاصة أوّلًا
- لا تُصلِح فورًا. ثبِّت أوّلًا السلوك الحاليّ بالاختبار. حتّى بلا وثيقة مواصفات، فإنّ مخرَج الكود العامل حاليًّا هو المواصفة نفسها.
- الأداة اللازمة لذلك هي اختبار التوصيف. اختبار يسجّل «السلوك الحاليّ» لا «السلوك الصحيح»، إذ يُحفَظ مخرَج التقرير وCSV ونتائج الحساب وغيرها كقيمة متوقَّعة كما هي، وتُقارَن الفروق قبل التغيير وبعده (أسلوب Golden Master).
- للبنى التي يتعذّر فيها إدخال اختبار (معالج أحداث واجهة مستخدم مكتوب مباشرةً، مرجع مباشر لـDateTime.Now أو مسار ملفّ)، اصنع «نقطة تماسّ (seam)» بأقلّ تغيير ممكن، عبر استخراج طريقة وحقن واجهة. لا حاجة لتعديل كبير.
- لا تخلط إعادة الهيكلة بإضافة الميزات في نفس الالتزام. شرط نجاح إعادة الهيكلة «فرق صفريّ»، وشرط نجاح إضافة الميزة «فرق مقصود فقط»، والخلط بينهما يجعل تمييز معنى الفرق مستحيلًا.
- يُحدَّد المدى الذي تُهيَّأ إليه الاختبارات بحسب حجم التعديل × عدد سنوات بقاء النظام × أثر الحادث عند الفشل. تركيب اختبارات وحدة على كلّ شيء ليس صحيحًا دائمًا، وهناك مواقف يكون فيها «اختبار التوصيف فقط» أو «عدم اللمس» هو القرار الصحيح.
- يمكن البدء حتّى بلا CI. مجرّد وجود مشروع اختبار واحد ومجلّد لملفّات القيمة المتوقَّعة يُحدِث فرقًا كبيرًا في مستوى الأمان، حتّى لو اقتصر التشغيل على الجهاز المحليّ.
2. لماذا «يُصاب الكود القديم بالكسر عند لمسه»
ليس سبب خوف تعديل الكود القديم هو قِدَم الكود. السبب هو عدم وجود وسيلة للتأكّد من صحّة نتيجة التغيير.
عرَّف مايكل فيذرز (Michael Feathers) في كتابه «Working Effectively with Legacy Code» الكود القديم لا بكونه «مجرّد كود قديم»، بل بكونه «كود بلا اختبارات».1 والسبب هو أنّه بلا اختبارات، لا توجد وسيلة سريعة للتأكّد في كلّ تعديل ممّا إذا كان الكود يتحسّن أم يسوء. وبحسب هذا التعريف، فحتّى الكود المكتوب بالأمس، إن لم يملك اختبارات، فهو كود قديم.
في الكود الخالي من الاختبارات، تبدأ الحلقة المفرغة التالية بالدوران.
- بلا اختبارات، لا يمكن معرفة نطاق أثر التعديل، فيصبح الأمر مخيفًا
- بسبب الخوف، يُكتفى بلصق أدنى حدّ وإضافة فروع شرطيّة بدل إصلاح البنية القائمة
- تتراكم الإصلاحات المؤقّتة، فيصبح الكود أقلّ قابليّةً للقراءة وأكثر هشاشةً
- مع تزايد الهشاشة، يزداد الخوف أكثر (العودة إلى 1)
مدخل قطع هذه الحلقة المفرغة ليس «إعادة هيكلة كبيرة بشجاعة». الترتيب معكوس: انصب شبكة الأمان (الاختبار) أوّلًا، وأزِل سبب الخوف، ثمّ أصلِح. غير أنّ هناك هنا مشكلة الدجاجة والبيضة. لكتابة اختبار، تحتاج إلى بنية قابلة للاختبار. لكن لجعل البنية قابلةً للاختبار، يجب تعديل الكود (إعادة هيكلة). فينتهي الأمر إلى تعديل كود بلا اختبارات، بلا اختبارات.
لحلّ هذا التناقض، يُنفَّذ إصلاح الكود القديم بالترتيب التالي.1
- تثبيت السلوك الحاليّ من الخارج، مقتصرًا على محيط موضع التعديل فقط (اختبار التوصيف)
- داخل شبكة الأمان تلك، إجراء تغيير طفيف جدًّا احتمال كسره منخفض للغاية (كاستخراج طريقة)، لصنع منفذ لإدخال الاختبار
- حين تصبح البنية قابلة لكتابة اختبارات دقيقة، الشروع في التعديل المطلوب أصلًا (إعادة الهيكلة أو إضافة الميزات)
سنتناول الخطوتين 1 و2 بشكل ملموس في الفصول التالية.
3. اختبار التوصيف ── تسجيل «السلوك الحاليّ»
3.1 ما الفرق مع الاختبار العاديّ
يتحقّق الاختبار العاديّ من السلوك الصحيح بمعنى «ما ينبغي أن يكون عليه الأمر وفق المواصفة». اختبار التوصيف مختلف. يسجّل كيف يتصرّف الكود فعليًّا الآن، مع تعليق الحكم على صحّته من عدمه.
لنفترض مثلًا أنّ وثيقة المواصفات لا تذكر ما إذا كانت معالجة الكسور تقريبًا أم قطعًا. إذا كان الكود الحاليّ يعمل بالقطع، ويسير العمل به لعشر سنوات، فإنّ «كونه قطعًا» هو المواصفة الفعليّة على الأقلّ. يثبّت اختبار التوصيف هذا كما هو بصيغة «المخرَج الحاليّ هو كذا». حتّى لو كان ذلك خطأً (bug)، ثبِّته أوّلًا. تغيير السلوك (إصلاح العيب) يُنفَّذ لاحقًا بشكل منفصل كتغيير مقصود، بعد اكتمال شبكة الأمان.
3.2 خطوات أسلوب Golden Master
بالنسبة للكود القديم ذي وحدة المخرَج الكبيرة، فإنّ أسلوب Golden Master هو اختبار التوصيف الأعلى عائدًا مقابل التكلفة. الخطوات بسيطة.
- تحديد المخرَج الذي تُنتجه الوظيفة المُراد تعديلها (نصّ التقرير، CSV، قائمة نتائج الحساب وغيرها)
- تحضير بيانات إدخال تمثيليّة، وتشغيل الكود الحاليّ للحصول على المخرَج
- حفظ ذلك المخرَج كما هو كملفّ قيمة متوقَّعة (Golden Master) وإدراجه في المستودع
- من الآن فصاعدًا، تشغيل الاختبار في كلّ مرّة يُعدَّل فيها الكود، والتحقّق من أنّ الفرق بين المخرَج وملفّ القيمة المتوقَّعة صفر
يكفي في التنفيذ بلغة C# شيء بسيط كالتالي، لا يعتمد على مكتبة معيّنة.
[Fact]
public void قائمة_الفواتير_الشهرية_Golden_Master()
{
// 1. قراءة إدخال تمثيليّ (بيانات مأخوذة من الإنتاج بعد إخفاء الهويّة وغيره)
var input = File.ReadAllLines(TestDataPath("billing-input-202606.csv"));
// 2. استدعاء المنطق القائم كما هو، للحصول على نصّ المخرَج
string actual = BillingReport.Generate(input);
// 3. عدم وجود ملفّ القيمة المتوقَّعة يعني إمّا «بيئة الاختبار معطوبة» أو «التشغيل الأوّل».
// في الحالتين، لا تمرِّره بصمت، بل اترك سجلًّا فقط وأفشِله وجوبًا
string expectedPath = TestDataPath("billing-expected-202606.txt");
if (!File.Exists(expectedPath))
{
File.WriteAllText(expectedPath + ".candidate", actual);
Assert.Fail("ملفّ القيمة المتوقَّعة غير موجود. راجِع محتوى .candidate، " +
"وإن لم توجد مشكلة، التزمه كقيمة متوقَّعة.");
}
// 4. التحقّق من التطابق التامّ مع السلوك المحفوظ
string expected = File.ReadAllText(expectedPath);
Assert.Equal(expected, actual);
}
تجنَّب التنفيذ الذي يحفظ المخرَج الحاليّ كما هو كقيمة متوقَّعة عند عدم وجود ملفّ القيمة المتوقَّعة، وينجح الاختبار بذلك. فإذا حدث سهو في التزام القيمة المتوقَّعة أو خطأ في وضع ملفّات بيئة الاختبار، فإنّ CI سيمرّ باللون الأخضر دون أن يكتشف التراجع. اجعل التسجيل الأوّل باتّجاه واحد فقط، بإخراج ملفّ مرشَّح (.candidate) كما في المثال أعلاه وإفشال الاختبار صراحةً، ثمّ يراجعه إنسان قبل التزامه كقيمة متوقَّعة.
بما أنّه يصعب تتبّع الفرق عند ظهوره من خلال رسالة Assert.Equal وحدها، فإنّ الممارسة العمليّة تقضي بكتابة المخرَج الفعليّ عند الفشل إلى ملفّ منفصل مثل billing-actual-202606.txt، بحيث يمكن مقارنته بالقيمة المتوقَّعة عبر أداة فروق مثل WinMerge، وهذا يسرِّع التحقيق.
3.3 اختيار الإدخال وتطبيع المخرَج
اختر الإدخال بمبدأ «التمثيليّ + الحدوديّ». أضِف حالة أو حالتين اعتياديّتين، ثمّ أضِف تدريجيًّا مدخلات تمرّ عبر الفروع التي وجدتَها بقراءة الكود، مثل إغلاق نهاية الشهر، والعدد صفر، والقيم السالبة، ومعالجة استثنائيّة لطرف تجاريّ معيّن. إذا أمكن استخدام بيانات الإنتاج بعد إخفاء الهويّة، فهذا ما يمرّ عبر أكبر عدد من الفروع الواقعيّة.
طبِّع القيم غير الحتميّة المختلطة في المخرَج قبل المقارنة. فوقت الطباعة، ووقت المعالجة، وGUID، والترقيم التلقائيّ، تتغيّر في كلّ تشغيل، فتُظهِر فرقًا في كلّ مرّة إن تُركت كما هي. بعد توليد المخرَج، أدرِج معالجة مسبقة تستبدل مثلًا وقت الطباعة: 2026/07/17 16:00 بـ وقت الطباعة: <DATE> عبر تعبير نمطيّ، قبل المقارنة.
نرتّب معيارًا لأيّ نوع من المخرَجات يصلح للنسخة المرجعيّة (Golden Master).
| نوع المخرَج | الملاءمة | ملاحظة |
|---|---|---|
| CSV والملفّات ثابتة الطول | ◎ | يمكن حفظها ومقارنتها كما هي. الهدف الأوّل الذي يجب استهدافه |
| التقرير (نصّ، البيانات الأصليّة لمعاينة الطباعة) | ◎ | تُلتقَط السلسلة النصّيّة قبل التحويل إلى PDF. تجنَّب مقارنة ثنائيّات PDF |
| قائمة نتائج الحساب (المبالغ، أعداد المخزون وغيرها) | ◎ | يمكن أيضًا إضافة طريقة مخصّصة للاختبار تُخرج النتائج إلى CSV وغيره |
| محتوى مكتوب في قاعدة البيانات | ○ | يُستعلَم (SELECT) عن محتوى الجدول بعد الكتابة ويُحوَّل إلى CSV للمقارنة |
| العرض على الشاشة نفسه | △ | ممكن إن أمكن تحويله إلى نصّ. أتمتة تشغيل الشاشة تحتاج عدّة أدوات مختلفة تناولناها في «الاختبار الآليّ لواجهة تطبيق سطح مكتب Windows» |
| الإرسال إلى نظام خارجيّ | △ | تحتاج إلى نقطة تماسّ (الفصل التالي) تلتقط البيانات مباشرةً قبل الإرسال |
4. كيفيّة صنع «نقطة التماسّ (seam)» لإدخال الاختبار
عند الشروع في كتابة Golden Master، يصطدم الكود القديم في كثير من الأحيان بجدار. فالمنطق مكتوب مباشرةً داخل معالج حدث واجهة المستخدم، ولا يمكن تنفيذه إلّا بتشغيل الشاشة. وهنا نحتاج إلى موضع يمكن من خلاله استبدال السلوك ومراقبته من كود الاختبار، وهو ما يسمّيه فيذرز نقطة التماسّ (seam).1
4.1 فصل المنطق عن واجهة المستخدم باستخراج طريقة
الحالة النمطيّة «قبل» هي هذه. الحساب والوصول إلى قاعدة البيانات والاعتماد على الوقت وتحديث الشاشة، كلّها مجتمعة في معالج حدث واحد.
// قبل: كلّ شيء مكتوب مباشرةً داخل معالج الحدث
private void btnCalc_Click(object sender, EventArgs e)
{
var rows = LoadRowsFromDb(); // وصول مباشر إلى قاعدة البيانات
var now = DateTime.Now; // اعتماد على الوقت الحاليّ
decimal total = 0;
foreach (var row in rows)
{
if (row.SalesDate.Year == now.Year &&
row.SalesDate.Month == now.Month) // تجميع الشهر الحاليّ فقط
{
total += Math.Floor(row.Amount * 1.1m); // قاعدة عمل هي معالجة الكسور
}
}
lblTotal.Text = total.ToString("N0"); // انعكاس مباشر على الشاشة
}
على هذا الحال، يحتاج اختبار منطق تجميع الشهر الحاليّ إلى الشاشة وقاعدة البيانات و«تاريخ اليوم» معًا. القاعدة القياسيّة لجعله قابلًا للاختبار بأقلّ تغيير هي استخراج جزء الحساب فقط إلى طريقة، وتحويل الاعتماديّات الخارجيّة (نتيجة قاعدة البيانات والوقت الحاليّ) إلى مُعطيات (arguments). استخدام ميزة استخراج الطريقة في Visual Studio (Ctrl+R, M) يقلّل أيضًا من أخطاء إعادة الكتابة اليدويّة.2
// بعد: استخراج الحساب فقط، مع استقبال «نتيجة قاعدة البيانات» و«الوقت الحاليّ» كمُعطيات
internal static decimal CalcMonthlyTotal(IEnumerable<SalesRow> rows, DateTime now)
{
decimal total = 0;
foreach (var row in rows)
{
if (row.SalesDate.Year == now.Year &&
row.SalesDate.Month == now.Month)
{
total += Math.Floor(row.Amount * 1.1m);
}
}
return total;
}
private void btnCalc_Click(object sender, EventArgs e)
{
var rows = LoadRowsFromDb();
lblTotal.Text = CalcMonthlyTotal(rows, DateTime.Now).ToString("N0");
}
يصبح جانب معالج الحدث ثلاثة أسطر «قراءة → حساب → عرض»، ويمكن اختبار الطريقة المُستخرَجة بأيّ بيانات صفوف وأيّ تاريخ. حتّى الحدود المرتبطة بالوقت كنهاية الشهر وبدايته والسنة الكبيسة يمكن إعادة إنتاجها بمجرّد تمرير تاريخ مثل new DateTime(2028, 2, 29).
4.2 جعل الاعتماديّات قابلة للاستبدال بحقن الواجهات
الاعتماديّات التي لا يكفي معها التحويل إلى مُعطيات (كالإشارة إلى DateTime.Now في مواضع متعدّدة، أو مسار ملفّ مكتوب مباشرةً) تُلفّ داخل واجهة (interface) وتُحقَن. يذكر دليل أفضل ممارسات اختبار الوحدة في .NET الصادر عن Microsoft Learn أيضًا أنّ الاعتماد المباشر على DateTime.Now مثال نمطيّ لا يمكن التحكّم به من الاختبار، ويعرض طريقة إدخال نقطة تماسّ (seam) عبر لفّه بواجهة.3
public interface IClock
{
DateTime Now { get; }
}
public sealed class SystemClock : IClock
{
public DateTime Now => DateTime.Now;
}
// في جانب الاختبار، يُحقَن تنفيذ يُعيد وقتًا ثابتًا
public sealed class FixedClock : IClock
{
private readonly DateTime _fixed;
public FixedClock(DateTime value) => _fixed = value;
public DateTime Now => _fixed;
}
بما أنّ إضافة IClock إلى مُنشئ صنف قائم يستلزم إصلاح جميع جهات الاستدعاء، فإنّ الواقعيّ في فترة الانتقال هو مرافقة ذلك بمُنشئ ذي قيمة افتراضيّة يستخدم SystemClock عند عدم وجود مُعطيات، ثمّ إصلاح جهات الاستدعاء تدريجيًّا. المسار نفسه ينطبق أيضًا على مسار الملفّ ونصّ الاتّصال بقاعدة البيانات المكتوبَين مباشرةً، بلفّهما في واجهة صغيرة تقتصر على عمليّتَي «القراءة والكتابة».
يُذكَر أنّ Visual Studio يتضمّن أيضًا ميزة إعادة هيكلة لاستخراج واجهة من صنف قائم (Extract Interface)، يمكن من خلالها إجراء هذا النوع من التعديل بشكل آليّ.2
المبدأ الذي يجب الحفاظ عليه عند صنع نقطة التماسّ واحد. التغيير الذي يصنع نقطة التماسّ نفسه يجب ألّا يغيّر السلوك ولو بمقدار مليمتر واحد. استخراج الطريقة وحقن الواجهة كلاهما عمليّتان يمكن إجراؤهما آليًّا بدعم المُصرِّف والبيئة التطويريّة، وبدرجة عالية من الحفاظ على السلوك. في هذه المرحلة قد تشعر برغبة في إصلاح المنطق «بالمناسبة»، لكنّ ذلك عمل يأتي بعد نصب شبكة الأمان.
5. جدول القرار حول مدى العمل المطلوب
يحتاج اختبار التوصيف وصنع نقطة التماسّ أيضًا إلى ساعات عمل. إعداد المستوى نفسه من الاختبارات لكلّ الكود القديم غير واقعيّ في المواقع صغيرة ومتوسّطة الحجم، ولا حاجة لذلك أصلًا. محاور القرار ثلاثة.
- حجم التعديل: هل هو إصلاح عيب من بضعة أسطر، أم إضافة ميزة، أم يتضمّن تغيير بنية؟
- عدد سنوات بقاء النظام: هل من المقرَّر ترحيله أو إيقافه خلال عام أو عامين، أم سيُستخدَم لخمس سنوات فما فوق؟
- أثر الحادث عند الفشل: هل هو بحدّ تشوّه في مظهر التقرير، أم خطأ في مبلغ الفاتورة أو عدد المخزون؟
| حجم التعديل | عدد سنوات البقاء | أثر الحادث | المستوى الموصى به |
|---|---|---|---|
| طفيف (بضعة أسطر / تغيير قيمة إعداد) | قصير (~عامان) | صغير (تشوّه عرض فقط تقريبًا) | اختبار التوصيف فقط. تثبيت المخرَج المعنيّ، التعديل، والانتهاء بالتحقّق من الفرق |
| طفيف إلى متوسّط | قصير | كبير (يتعلّق بالمبالغ أو المخزون) | اختبار التوصيف فقط بشكل أثقل. زيادة أنماط الإدخال بما يشمل الحدود |
| متوسّط (إضافة ميزة / تغيير منطق) | طويل (5 سنوات فأكثر) | صغير إلى متوسّط | اختبار التوصيف + إعداد اختبار وحدة مقتصر على محيط موضع التعديل (صنع نقطة تماسّ) |
| متوسّط إلى كبير | طويل | كبير | اختبار التوصيف + إعداد اختبار الوحدة + تقسيم وحدة الإصدار إلى أجزاء أصغر |
| كبير (يتطلّب تجديد البنية) | قصير | ─ | لا تلمس. لا تُعدِّل، اتّبع تحايلًا تشغيليًّا، ووجِّه ساعات العمل نحو الترحيل أو الاستبدال |
| ─ (لا يوجد طلب تعديل أصلًا) | ─ | ─ | لا تلمس. لا تُجرِ إعادة هيكلة وقائيّة على كود عامل |
السطران الأخيران «لا تلمس» ليسا خيارًا سلبيًّا، بل قرار إيجابيّ. الاستثمار في الجودة الداخليّة لنظام قصير مدّة البقاء لا يُستردّ. تلك الساعات ينبغي توجيهها إلى قرار الترحيل المرتَّب في «جدول قرار إطالة عمر تطبيقات VB6 / Access وترحيلها» وإلى تصميم الوجهة المُرحَّل إليها.
كما أنّه حتّى عند التقدّم إلى «إعداد اختبار الوحدة»، يلزم رسم خطّ فاصل بين ما يُكتَب كاختبار وحدة وما يُترَك كاختبار تكامل (اختبار يستخدم قاعدة بيانات وملفّات حقيقيّة). هذا الفصل مرتَّب كجدول قرار في «كيف نرسم الحدّ بين اختبار الوحدة واختبار التكامل»، فراجعه أيضًا. وبما أنّ اختبار الوحدة يُفترَض أن يكون سريعًا ومعزولًا وقابلًا للتكرار،3 فمن الحكمة فصل اختبار التوصيف الذي يلامس قاعدة البيانات أو الملفّات في مشروع ووحدة تنفيذ منفصلة عن اختبار الوحدة.
6. قواعد التشغيل ── حتّى لا تنكسر شبكة الأمان
اختبار التوصيف يفقد معناه بسهولة إن أُخطئ تشغيله بعد كتابته. نقتصر على ثلاث قواعد دنيا.
6.1 لا تخلط إعادة الهيكلة بإضافة الميزات في نفس الالتزام
إعادة الهيكلة هي تغيير يجعل الكود أسهل فهمًا وصيانةً دون تغيير السلوك.4 بعبارة أخرى، شرط النجاح هو فرق صفريّ مع النسخة المرجعيّة (Golden Master). أمّا شرط نجاح إضافة الميزة أو إصلاح العيب فهو ظهور الفرق المقصود فقط. إذا خُلِط هذان في التزام (commit) واحد، يتعذّر عند ظهور فرق تحديد ما إذا كان «تغييرًا مقصودًا» أو «كسرًا».
| نوع التغيير | التعامل مع النسخة المرجعيّة | شرط النجاح |
|---|---|---|
| إعادة الهيكلة (تغيير البنية) | لا تُحدَّث | فرق صفريّ |
| إصلاح عيب / إضافة ميزة (تغيير السلوك) | تُحدَّث بعد مراجعة الفرق | الفرق المقصود فقط |
| صنع نقطة تماسّ (استخراج طريقة / حقن واجهة) | لا تُحدَّث | فرق صفريّ |
| تغيير قاعدة تطبيع القيمة المتوقَّعة | تُعاد توليدها | يُذكَر سبب التغيير في رسالة الالتزام |
الأمر نفسه ينطبق على وحدة الإصدار (release). بما أنّ «إصدارًا يقتصر على إعادة الهيكلة» يُفترَض ألّا يتغيّر فيه السلوك، فإذا ظهر عطل يمكن الاشتباه فورًا بإعادة الهيكلة. إذا خُلِطا، لا يعمل هذا الفصل.
6.2 تحديث القيمة المتوقَّعة بترتيب «مراجعة الفرق ثمّ الاستبدال»
عند تغيير السلوك عمدًا، تُحدَّث النسخة المرجعيّة أيضًا. ثبِّت الخطوات.
- توليد المخرَج بعد التغيير، ومراجعة الفرق مع القيمة المتوقَّعة الحاليّة بصريًّا
- التأكّد من أنّ الفرق يقتصر على التغيير المقصود فقط (إن تغيّر ولو سطر واحد غير مقصود، يُحقَّق فيه)
- استبدال ملفّ القيمة المتوقَّعة بالمخرَج الجديد، وإدراجه في نفس التزام الكود ليبقى في السجلّ التاريخيّ
الخطر هو تشغيل من نوع «استبدال القيمة المتوقَّعة لتحويل الاختبار إلى أخضر لمجرّد أنّه أصبح أحمر». إذا فُعِل ذلك، يُسجَّل التراجع كما هو باعتباره «صحيحًا»، وتتوقّف شبكة الأمان عن كونها شبكة أمان.
6.3 إعداد حدّ أدنى يعمل حتّى بلا CI
حتّى في المواقع التي لا يوجد فيها خادم CI، يمكن البدء اليوم بالحدّ الأدنى التالي.
- إضافة مشروع اختبار واحد إلى الحلّ (يعمل MSTest / NUnit / xUnit جميعًا حتّى مع بقاء .NET Framework)
- وضع ملفّات القيمة المتوقَّعة وبيانات الإدخال في مجلّد
TestData، وإدارة إصدارها مع الكود - جعل تشغيل
dotnet test(أو مستكشف الاختبارات في Visual Studio) يدويًّا قبل الالتزام وعدًا يلتزم به الفريق - إدراج سطر واحد في دليل إجراءات الإصدار «تشغيل الاختبار والتأكّد من فرق صفريّ» حتّى لا يُنسى التحقّق من نتيجة التشغيل
عند مطابقة نتيجة تشغيل الاختبار مع مخرَج جانب التطبيق، كلّما كانت السجلّات مُعدَّة جيّدًا، تسارَع التحقيق في السبب. ما ينبغي تسجيله في السجلّ مشروح في «الحدّ الأدنى من متطلّبات مسجّل ذاتيّ الصنع وقائمة تحقّق اختبار التكامل».
7. الخلاصة
- الكود القديم هو «كود بلا اختبارات»،1 والسبب الحقيقيّ لكونه يُصاب بالكسر عند لمسه هو عدم وجود وسيلة للتأكّد من نتيجة التغيير. قبل الإصلاح، ثبِّت السلوك الحاليّ بالاختبار.
- اختبار التوصيف هو اختبار يسجّل «السلوك الحاليّ» لا «السلوك الصحيح». يمكن البدء بكود C# بسيط فقط عبر أسلوب Golden Master الذي يحفظ التقرير وCSV ونتيجة الحساب كما هي في ملفّ قيمة متوقَّعة ويقارن الفروق.
- للبنى التي يتعذّر إدخال اختبار فيها، اصنع نقطة تماسّ (seam) عبر استخراج طريقة وحقن واجهة. طريقة لفّ اعتماديّة مثل
DateTime.Nowقاعدة معروفة وردت أيضًا في إرشادات اختبار الوحدة الصادرة عن Microsoft.32 - يُحدَّد المدى الذي تُهيَّأ إليه الاختبارات بـحجم التعديل × عدد سنوات البقاء × أثر الحادث عند الفشل. «اختبار التوصيف فقط» و«عدم اللمس» أيضًا قراران معتبَران.
- في التشغيل، احرص على عدم خلط إعادة الهيكلة (شرط النجاح فرق صفريّ) وإضافة الميزة (شرط النجاح الفرق المقصود فقط)،4 وعلى مرور تحديث القيمة المتوقَّعة عبر مراجعة الفرق دائمًا. حتّى بلا CI، مجرّد الالتزام بتشغيل الاختبار يدويًّا يُحدِث فرقًا كبيرًا في مستوى الأمان.
مقالات ذات صلة
- كيف نرسم الحدّ بين اختبار الوحدة واختبار التكامل
- إلى متى ستستمرّ تطبيقات VB6 في العمل ── وضع دعم بيئة التشغيل والمسار العمليّ للترحيل إلى .NET
- إطالة عمر تطبيقات VB6 / Access وترحيلها ── جدول قرار الإبقاء والتغليف والاستبدال
- الحدّ الأدنى من متطلّبات مسجّل ذاتيّ الصنع وقائمة تحقّق اختبار التكامل
- الاختبار الآليّ لواجهة تطبيق سطح مكتب Windows
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت المحدودة مع إدخال اختبار التوصيف على تطبيقات أعمال قائمة بلا اختبارات، وإعادة الهيكلة التدريجيّة نحو بنية قابلة للاختبار، وترتيب القرار حول الاستثمار في التعديل أم الترحيل.
- التعديل والصيانة لبرامج Windows القائمة
- الاستفادة من الأصول القائمة ودعم الترحيل
- الاستشارة التقنيّة ومراجعة التصميم
- التواصل معنا
روابط مرجعيّة
-
Michael C. Feathers، “Working Effectively with Legacy Code” (Prentice Hall، 2004). حول تعريف الكود القديم بأنّه «كود بلا اختبارات»، وخطوات الشروع في التعديل بعد تسجيل السلوك الحاليّ عبر اختبار التوصيف (characterization test)، ومفهوم نقطة التماسّ (seam) لإدخال الاختبار. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn، Extract and inline refactorings (Visual Studio). حول خطوات تشغيل ميزة استخراج طريقة (Ctrl+R, M) واستخراج واجهة (Extract Interface) الخاصّتين بـC# / Visual Basic في Visual Studio. ↩ ↩2 ↩3
-
Microsoft Learn، Unit testing best practices for .NET. حول خصائص اختبار الوحدة الجيّد (سريع / معزول / قابل للتكرار / ذاتيّ التحقّق / في وقته المناسب)، وطريقة إدخال نقطة تماسّ (seam) بلفّ اعتماديّة لا يمكن التحكّم بها مثل
DateTime.Nowداخل واجهة، وضرورة فصل الاعتماديّة على البنية التحتيّة عن اختبار الوحدة إلى اختبار التكامل. ↩ ↩2 ↩3 -
Microsoft Learn، Refactor code (Visual Studio). حول تعريف إعادة الهيكلة بوصفها عمليّة تغيير الكود لجعله أسهل صيانةً وفهمًا وتوسيعًا دون تغيير السلوك. ↩ ↩2
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
إلى متى ستستمرّ تطبيقات VB6 في العمل ── وضع دعم بيئة التشغيل والمسار العمليّ للترحيل إلى .NET
إلى متى تستمرّ تطبيقات VB6 في العمل؟ يُنظّم هذا المقال التناقض بين سياسة دعم بيئة تشغيل VB6 (المدعومة حتّى على Windows 11) وحقيقة انتهاء ...
إدارة إصدارات مخطّط قاعدة بيانات تطبيقات الأعمال ── ممارسات الترحيل (migration) لمنع «اختلاف قاعدة البيانات من عميل لآخر»
دليل عمليّ لإدارة إصدارات مخطّط قاعدة بيانات تطبيقات الأعمال الموزَّعة على عملاء متعدّدين. نشرح PRAGMA user_version وتنفيذ الترحيل الأمام...
عندما ترث نظاماً بلا شيفرة مصدريّة ولا مواصفات ── الإجراءات العمليّة لتشغيله وصيانته دون توقّف
نُنظِّم الإجراءات العمليّة لبدء تشغيل وصيانة نظام أعمال بلا شيفرة مصدريّة ولا مواصفات. نشرح صون البيئة العاملة والنسخ الاحتياطيّ، وجرد ال...
كيفيّة إنشاء خدمات Windows وتشغيلها ── من التمييز عن جدولة المهام إلى تحويل BackgroundService إلى خدمة
هل ينبغي تحويل المعالجة المقيمة إلى خدمة Windows، أم تكفي جدولة المهام؟ نرتّب من منظور عمليّ جدول القرار، وكيفيّة إنشاء الخدمة عبر .NET W...
كيف تختار مكان حفظ بيانات تطبيق Windows محليّاً ── جدول قرار بين SQLite وJSON وRegistry وAccess
أين ينبغي حفظ بيانات تطبيق سطح مكتب Windows، وبأيّ صيغة؟ نرتّب الفرق بين AppData وProgramData، ونقاط قوّة وعيوب كلّ من SQLite وملفّات JSO...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- ما هو اختبار التوصيف (characterization test)؟
- هو اختبار يسجّل «السلوك الحاليّ» كما هو، لا «السلوك الصحيح». في الكود القديم الذي لم تعد وثائق مواصفاته محفوظة، لا تتوفّر غالبًا وسيلة للتأكّد ممّا هو صحيح، لذا يُحفَظ أوّلًا مخرج الكود العامل حاليًّا (التقارير، ملفّات CSV، نتائج الحسابات وغيرها) كقيمة متوقّعة، ويُتحقَّق آليًّا من عدم تغيّر المخرج قبل التعديل وبعده. الاستخدام الأساسيّ هو نصب شبكة أمان تُثبِّت السلوك، ثمّ الانتقال إلى إعادة الهيكلة أو إضافة الميزات.
- من أين نبدأ في كود قديم لا يحتوي على أيّ اختبارات؟
- الأمر الواقعيّ هو كتابة اختبارات التوصيف مقتصرةً فقط على محيط الموضع الذي سيُعدَّل قريبًا. تغطية النظام بأكمله بالاختبارات غير ممكنة من حيث حجم العمل في معظم الحالات، ولا حاجة لذلك أصلًا. حدِّد أوّلًا المخرَج الذي تُنتجه الوظيفة المُراد تعديلها (تقرير، CSV، محتوى مكتوب في قاعدة البيانات وغيرها)، واحفظ في ملفّ مخرَج مدخلات تمثيليّة لتثبيته. داخل شبكة الأمان تلك، أجرِ إعادة هيكلة صغيرة كاستخراج طريقة (method extraction)، واستخرج المنطق إلى شكل قابل للاختبار قبل الشروع في التعديل الأصليّ.
- لماذا لا يجوز خلط إعادة الهيكلة بإضافة الميزات في نفس الالتزام (commit)؟
- لأنّه عند ظهور فرق في المخرَج، يتعذَّر فصل السبب. إعادة الهيكلة عمل يتحقّق فيه من «عدم تغيّر السلوك»، وإضافة الميزة عمل يتحقّق فيه من «تغيّر السلوك في الموضع المقصود فقط»، وشرط النجاح في الحالتين متضادّ تمامًا. فإذا خُلِطا، يتعذّر التمييز بين كون الفرق مع النسخة المرجعيّة (Golden Master) «تغييرًا مقصودًا» أو «كسرًا». من الآمن التحقّق بشكل منفصل: فرق صفريّ في التزام إعادة الهيكلة، وفرق مقصود فقط في التزام إضافة الميزة.
- متى تُحدَّث النسخة المرجعيّة (ملفّ القيمة المتوقَّعة)؟
- فقط عند لحظة تغيير السلوك عمدًا، أي عند التزام إضافة ميزة أو إصلاح عيب. عند التحديث، راجِع بصريًّا الفرق بين المخرَج قبل التغيير وبعده، وتأكَّد من أنّه لا يحتوي إلّا على التغيير المقصود، ثمّ استبدل القيمة المتوقَّعة بالمخرَج الجديد. فإذا استُبدِلت القيمة المتوقَّعة آليًّا لمجرّد أنّ الاختبار أصبح أحمر، سيُدمَج التراجع (تغيّر سلوك غير مقصود) كما هو باعتباره «صحيحًا»، وتفقد شبكة الأمان معناها بوصفها شبكة أمان.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة