التعديل الآمن على تطبيق أعمال قديم بلا اختبارات ── ممارسة اختبار التوصيف وإعادة الهيكلة
· آخر تحديث: · غو كومورا · التقنيات القديمة, الاستفادة من الأصول القائمة, إعادة الهيكلة, تصميم الاختبارات, اختبار التوصيف, C#, .NET, الصيانة, جدول القرار, الاستشارات التقنية
سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621711)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
غو كومورا (2026). التعديل الآمن على تطبيق أعمال قديم بلا اختبارات ── ممارسة اختبار التوصيف وإعادة الهيكلة. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621711 https://comcomponent.com/ar/blog/characterization-test-legacy-refactoring/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621711
- DOI (هذه النسخة)
- 10.5281/zenodo.22241046
«أعرف الموضع الذي أريد إصلاحه. لكن حين أفكّر أنّ لمسي له قد يكسر موضعاً آخر، لا أستطيع مدّ اليد» ── عبارة نسمعها كثيراً ممّن ورثوا تطبيق أعمال بلا اختبارات.
كثير من تطبيقات الأعمال المكتوبة بـ VB6 أو .NET Framework أو Access لا تملك اختبارات آليّة. توقّف تحديث وثائق المواصفات أيضاً، فتصبح الحالة «الكود هو وثيقة المواصفات الوحيدة». ومع ذلك يستمر العمل، ولا تنتظر طلبات التعديل: تغيير نسبة ضريبة الاستهلاك، تصحيح تخطيط التقرير، إضافة طرف تجاري.
تناولنا في المدوّنة في «إلى متى ستستمر تطبيقات VB6 في العمل ── وضع دعم بيئة التشغيل والمسار العملي للترحيل إلى .NET» فكرة الترحيل بمطابقة مخرجات النظام القديم باعتباره «وثيقة مواصفات عاملة». يطبّق هذا المقال الفكرة نفسها على موقف ليس الترحيل، بل التعديل الفوري على الكود العامل الآن في مكانه. الأداة المحوريّة اختبار التوصيف (characterization test). حتّى الكود بلا اختبارات، إذا ثبّت أوّلاً بالاختبار السلوك الذي قد تكسره لاحقاً، صارت إعادة الهيكلة وإضافة الميزات أكثر أماناً بمراحل.
القرّاء المفترضون والمقدّمات كالتالي. خبرة الاختبار الآلي غير لازمة؛ المحتوى يبدأ ممّن لم يكتب اختباراً واحداً. اللازم مقدّمتان فقط: (1) أن تستطيع بناء التطبيق المستهدف بنفسك (المصدر وبيئة تطوير يمرّ فيها البناء في متناولك)، (2) أن تكون في موقع يسمح بتعديل الشيفرة. أمثلة الشيفرة بـ C# (كتابة تعمل على .NET Framework و.NET)، لكن الفكرة لا تتقيّد بلغة. بالمقابل، إن لم يوجد مصدر أو تعذّر البناء، لا تُستخدم أساليب هذا المقال كما هي؛ تلزم تهيئة سابقة.
1. الخلاصة أوّلاً
- لا تُصلح فوراً. ثبّت أوّلاً السلوك الحالي بالاختبار. حتّى بلا وثيقة مواصفات، مخرج الكود العامل الآن هو المواصفة نفسها.
- الأداة لذلك اختبار التوصيف. اختبار يسجّل «السلوك الحالي» لا «السلوك الصحيح»: يحفظ مخرج التقرير وCSV ونتائج الحساب كما هو كقيمة متوقّعة، ويقارن الفرق قبل التغيير وبعده (أسلوب Golden Master).
- للبنى التي لا يُدخَل فيها اختبار (معالج حدث واجهة مكتوب مباشرة، مرجع مباشر لـ
DateTime.Nowأو مسار ملف)، اصنع «نقطة تماس (seam)» بأقل تغيير: استخراج طريقة وحقن واجهة. لا حاجة لتعديل كبير. - لا تخلط إعادة الهيكلة بإضافة الميزات في الالتزام نفسه. شرط نجاح إعادة الهيكلة «فرق صفري»، وشرط نجاح إضافة الميزة «فرق مقصود فقط»؛ الخلط يجعل تمييز معنى الفرق مستحيلاً.
- مدى تجهيز الاختبارات يُقرَّر بـ حجم التعديل × سنوات بقاء النظام × أثر العطل. تغطية كل شيء باختبارات وحدة ليست دائماً الإجابة الصحيحة؛ أحياناً «اختبار التوصيف فقط» أو «عدم اللمس» هو القرار الصحيح.
- يمكن البدء بلا CI. مشروع اختبار واحد ومجلّد لملفّات القيمة المتوقّعة يغيّران مستوى الأمان كثيراً، حتّى إن اقتصر التشغيل على الجهاز المحلّي.
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 19، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. لماذا «ينكسر الكود القديم عند لمسه»
خوف تعديل الكود القديم ليس لأنّ الكود قديم. السبب عدم وجود وسيلة للتأكّد من أنّ نتيجة التغيير صحيحة.
عرّف مايكل فيذرز (Michael Feathers) في كتابه Working Effectively with Legacy Code الكود القديم لا بوصفه «مجرّد كود قديم»، بل بوصفه «كوداً بلا اختبارات».1 بلا اختبارات لا توجد طريقة سريعة في كل تعديل لمعرفة إن كان الكود يتحسّن أم يسوء. بحسب هذا التعريف، حتّى كود كُتب أمس كود قديم إن لم يملك اختبارات.
في الكود بلا اختبارات تبدأ الحلقة التالية بالدوران.
- بلا اختبارات لا يُعرَف نطاق أثر التعديل، فيصبح الأمر مخيفًا
- بسبب الخوف يُكتفى بلصق أدنى حدّ وإضافة فروع شرطيّة بدل إصلاح البنية القائمة
- تتراكم الإصلاحات المؤقّتة فيصبح الكود أقل قابليّة للقراءة وأكثر هشاشة
- مع تزايد الهشاشة يزداد الخوف (العودة إلى 1)
مدخل قطع هذه الحلقة ليس «إعادة هيكلة كبيرة بشجاعة». الترتيب معكوس: انصب شبكة الأمان (الاختبار) أوّلاً، أزِل سبب الخوف، ثم أصلح. غير أنّ هنا مشكلة الدجاجة والبيضة. لكتابة اختبار تحتاج بنية قابلة للاختبار. ولجعل البنية قابلة للاختبار يجب تعديل الكود (إعادة هيكلة). فينتهي الأمر إلى تعديل كود بلا اختبارات، بلا اختبارات.
لحل هذا التناقض يسير إصلاح الكود القديم بالترتيب التالي.1
- تثبيت السلوك الحالي من الخارج، مقتصراً على محيط موضع التعديل فقط (اختبار التوصيف)
- داخل شبكة الأمان تلك، إجراء تغيير طفيف احتمال كسره بالغ الانخفاض (كاستخراج طريقة)، لصنع منفذ لإدخال الاختبار
- حين تصبح البنية قابلة لكتابة اختبارات دقيقة، الشروع في التعديل المطلوب أصلاً (إعادة الهيكلة أو إضافة الميزات)
نتناول الخطوتين 1 و2 بشكل ملموس في الفصول التالية.
3. اختبار التوصيف ── تسجيل «السلوك الحالي»
3.1 ما الفرق مع الاختبار العادي
يتحقّق الاختبار العادي من السلوك الصحيح بمعنى «ما ينبغي أن يكون وفق المواصفة». اختبار التوصيف مختلف. يسجّل كيف يتصرّف الكود فعليّاً الآن، مع تعليق الحكم على صحّته.
لنفترض مثلاً أنّ وثيقة المواصفات لا تذكر إن كانت معالجة الكسور تقريباً أم قطعاً. إذا كان الكود الحالي يعمل بالقطع، وسار العمل به عشر سنوات، فإنّ «كونه قطعاً» هو المواصفة الفعليّة على الأقل. يثبّت اختبار التوصيف هذا كما هو بصيغة «المخرج الحالي كذا». حتّى لو كان ذلك عيباً، ثبّته أوّلاً. تغيير السلوك (إصلاح العيب) يُنفَّذ لاحقاً كتغيير مقصود منفصل، بعد اكتمال شبكة الأمان.
3.2 خطوات أسلوب Golden Master
للكود القديم ذي وحدة المخرج الكبيرة، أسلوب Golden Master أعلى اختبار توصيف عائداً مقابل التكلفة. الخطوات بسيطة.
- تحديد المخرج الذي تنتجه الوظيفة المستهدفة (نص التقرير، CSV، قائمة نتائج الحساب، إلخ)
- تحضير بيانات إدخال تمثيليّة، وتشغيل الكود الحالي للحصول على المخرج
- حفظ ذلك المخرج كما هو كملف قيمة متوقّعة (Golden Master) وإدراجه في المستودع
- من الآن، تشغيل الاختبار في كل تعديل، والتحقّق من أنّ فرق المخرج وملف القيمة المتوقّعة صفر
يكفي في التنفيذ بـ C# شيء بسيط كهذا، لا يعتمد على مكتبة معيّنة.
[Fact]
public void 月次請求一覧_ゴールデンマスター()
{
// 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؛ هذا يسرّع التحقيق.
لاحظ أنّ مثال الشيفرة يفترض أنّ BillingReport.Generate للمنطق القائم يمكن استدعاؤه كما هو من مشروع الاختبار. في مواقع الكود القديم كثيراً ما يعلق الأمر هنا أوّلاً، لذا لخّصنا طريقة الربط في القسم 3.4.
يختلف اسم الأسلوب حسب الأدبيّات: إلى جانب اختبار Golden Master يُدعى أيضاً اختبار الموافقة (approval testing) واختبار اللقطة (snapshot testing). توجد مكتبات تجسّد الفكرة نفسها؛ في .NET يمثّلها ApprovalTests.Net وVerify. تتولّى قواعد تسمية ملف القيمة المتوقّعة، وإطلاق أداة الفروق تلقائيّاً، وعملية الموافقة على القيمة المتوقّعة، أي ما صنعناه يدوياً في الشيفرة أعلاه. ابدأ بتنفيذ بسيط كالذي فوق، وفكّر في المكتبة حين يكثر عدد ملفّات القيمة المتوقّعة ويصعب إدارتها. عند البحث، «approval testing» و«snapshot testing» أوفر معلومات من «golden master».
3.3 اختيار الإدخال وتطبيع المخرج
اختر الإدخال بمبدأ «التمثيلي + الحدودي». أضف حالة أو حالتين اعتياديّتين، ثم أضف تدريجيّاً مدخلات تمرّ عبر الفروع التي وجدتها بقراءة الكود: إغلاق نهاية الشهر، صفر صفوف، قيم سالبة، معالجة استثنائيّة لطرف معيّن. إن أمكن استخدام بيانات الإنتاج بعد إخفاء الهويّة، فهذا ما يمرّ عبر أكبر عدد من الفروع الواقعيّة.
طبّع القيم غير الحتميّة المختلطة في المخرج قبل المقارنة. وقت الطباعة، ووقت المعالجة، وGUID، والترقيم التلقائي تتغيّر في كل تشغيل، فتُظهر فرقاً في كل مرّة إن تُركت كما هي. بعد توليد المخرج أدرج معالجة مسبقة تستبدل مثلاً 印刷日時: 2026/07/17 16:00 بـ 印刷日時: <DATE> بتعبير نمطي، ثم قارن.
نرتّب معياراً لأي نوع مخرج يصلح لـ Golden Master.
| نوع المخرج | الملاءمة | ملاحظة |
|---|---|---|
| CSV والملفّات ثابتة الطول | ◎ | تُحفَظ وتُقارَن كما هي. الهدف الأوّل الذي ينبغي استهدافه |
| التقرير (نص، البيانات الأصليّة لمعاينة الطباعة) | ◎ | تُلتقَط السلسلة قبل التحويل إلى PDF. تجنّب مقارنة ثنائيّات PDF |
| قائمة نتائج الحساب (مبالغ، أعداد مخزون، إلخ) | ◎ | يمكن أيضاً إضافة طريقة مخصّصة للاختبار تخرج النتائج إلى CSV |
| محتوى مكتوب في قاعدة البيانات | ○ | يُستعلَم (SELECT) عن محتوى الجدول بعد الكتابة ويُحوَّل إلى CSV للمقارنة |
| العرض على الشاشة نفسه | △ | ممكن إن أمكن تحويله إلى نص. أتمتة تشغيل الشاشة تحتاج عدّة أدوات أخرى تناولناها في «الاختبار الآلي لواجهة تطبيق سطح مكتب Windows» |
| الإرسال إلى نظام خارجي | △ | تحتاج نقطة تماس (الفصل التالي) تلتقط البيانات مباشرة قبل الإرسال |
3.4 جعل مشروع الاختبار قادراً على استدعاء التطبيق القائم
تطبيق WinForms / WPF القائم مشروع EXE. «أضفت مشروع اختبار لكن أصناف المتن لا تُرى منه» هو أوّل حاجز في إصلاح الكود القديم، لذا نرتّب طريقة الربط.
1. أضف مشروع اختبار واحد. أضف مشروع اختبار جديداً إلى الحل القائم. يمكن استخدام MSTest أو NUnit أو xUnit حتّى مع بقاء .NET Framework. اجعل إطار الهدف لمشروع الاختبار مطابقاً للمتن (إن كان المتن .NET Framework 4.8 فالاختبار أيضاً 4.8). إن اختلفا ظهرت تحذيرات أو أخطاء تحميل عند إضافة المرجع.
2. أضف مرجعاً إلى مشروع المتن. طريقان للربط، والأصل الأوّل.
| طريقة الربط | متى تُستخدم | كيف |
|---|---|---|
| مرجع مشروع (موصى به) | مصدر المتن موجود ويمكن بناؤه في الحل نفسه | انقر يميناً على مشروع الاختبار ← إضافة مرجع ← مشاريع ← اختر مشروع EXE للمتن. مشروع EXE تجميعة أيضاً ويمكن الرجوع إليه (قول «EXE فلا يمكن الرجوع» سوء فهم) |
| مرجع ملف DLL / EXE | لا يمكن إدخال المتن في الحل، ولا يوجد سوى ثنائي مبني | إضافة مرجع ← استعراض ← حدّد ملف EXE / DLL في bin للمتن مباشرة. انتبه ألّا يَقدُم المرجع في كل إعادة بناء للمتن |
اتّجاه المرجع اختبار → متن باتّجاه واحد فقط. مرجع المتن إلى الاختبار يصنع مرجعاً دائريّاً.
3. إن أردت الاختبار مع بقاء internal فاستخدم InternalsVisibleTo. كما في الفصل 4، عند استخراج المنطق بطريقة كثيراً ما تريد إبقاؤه internal (اختبار بلا توسيع واجهة عامّة). عندئذ أضف السمة التالية سطراً واحداً إلى تجميعة جانب المتن.2
// 本体側の AssemblyInfo.cs か、任意のソースファイルの先頭に置く
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("MyApp.Tests")]
نقطتا انتباه.2
- وحّد حالة التوقيع بين المتن والاختبار. إمّا كلاهما بلا توقيع، أو كلاهما باسم قوي. إن كان المتن باسم قوي فاكتب
InternalsVisibleTo("MyApp.Tests, PublicKey=0024...")بـالمفتاح العام الكامل (لا رمز المفتاح العام). يُستخرَج المفتاح العام بـsn -pوsn -tp. privateلا يظهر.InternalsVisibleToيسري علىinternal/protected internal/private protectedفقط. إن رغبت في اختبار طريقةprivateفهذه علامة أنّ الصنف أكبر ممّا ينبغي؛ الأوضح استخراجها بطريقة كما في الفصل 4.
4. تحقّق من موضع ملفّات البيانات. دليل العمل الحالي عند تشغيل الاختبار يصبح مجلّد خرج الاختبار (bin\Debug\...). ضع ملفّات القيمة المتوقّعة وCSV الإدخال في مجلّد TestData، واضبط خاصيّة «نسخ إلى دليل الخرج» على «نسخ إذا كان أحدث»، فيُكتَب TestDataPath في القسم 3.2 بسلاسة. إن كان جانب المتن يقرأ app.config أو ملف إعداد، قد يلزم إعداد مكافئ في مشروع الاختبار أيضاً.
إن اكتمل هذا، صار اختبار Golden Master في القسم 3.2 قابلاً للكتابة كما هو.
4. كيفيّة صنع «نقطة التماس (seam)» لإدخال الاختبار
عند الشروع في كتابة Golden Master يصطدم كثير من الكود القديم بجدار. المنطق مكتوب مباشرة داخل معالج حدث الواجهة، ولا يُنفَّذ إلّا بتشغيل الشاشة. هنا نحتاج موضعاً يمكن من خلاله استبدال السلوك ومراقبته من كود الاختبار، وهو ما يسمّيه فيذرز نقطة التماس (seam).1
4.1 فصل المنطق عن الواجهة باستخراج طريقة
الحالة النمطيّة «قبل» كالتالي. الحساب والوصول إلى قاعدة البيانات والاعتماد على الوقت وتحديث الشاشة مجتمعة في معالج حدث واحد.
// Before: すべてがイベントハンドラーに直書き
private void btnCalc_Click(object sender, EventArgs e)
{
var rows = LoadRowsFromDb(); // DB直アクセス
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"); // 画面へ直接反映
}
على هذا الحال، يحتاج اختبار منطق تجميع الشهر الحالي إلى الشاشة وقاعدة البيانات و«تاريخ اليوم» معاً. القاعدة القياسيّة لجعله قابلاً للاختبار بأقل تغيير: استخراج جزء الحساب فقط إلى طريقة، وتحويل الاعتماديّات الخارجيّة (نتيجة قاعدة البيانات والوقت الحالي) إلى وسائط. استخدام استخراج الطريقة في Visual Studio (Ctrl+R, M) يقلّل أيضاً أخطاء إعادة الكتابة اليدويّة.3
// After: 計算だけを抽出し、「DBの結果」と「現在時刻」を引数として受け取る
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 في مواضع كثيرة، أو مسار ملف مكتوب مباشرة) تُلفّ بواجهة وتُحقَن. يذكر دليل أفضل ممارسات اختبار الوحدة في .NET على Microsoft Learn أيضاً أنّ الاعتماد المباشر على DateTime.Now مثال نمطي لا يمكن التحكّم به من الاختبار، ويعرض إدخال نقطة تماس (seam) بلفّه بواجهة.4
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)، يمكن بها إجراء هذا النوع من التعديل آليّاً.3
المبدأ عند صنع نقطة التماس واحد. التغيير الذي يصنع نقطة التماس نفسه يجب ألّا يغيّر السلوك ولو بمقدار مليمتر. استخراج الطريقة وحقن الواجهة عمليّتان يمكن إجراؤهما آليّاً بدعم المصرّف وبيئة التطوير، وبدرجة عالية من حفظ السلوك. في هذه المرحلة قد ترغب في إصلاح المنطق «بالمناسبة»، لكن ذلك عمل يأتي بعد نصب شبكة الأمان.
5. جدول القرار: إلى أي مدى نذهب
اختبار التوصيف وصنع نقطة التماس يحتاجان جهداً أيضاً. إعداد المستوى نفسه من الاختبارات لكل الكود القديم غير واقعي في مواقع صغيرة ومتوسّطة، ولا حاجة إليه. محاور القرار ثلاثة.
- حجم التعديل: إصلاح عيب ببضعة أسطر، أم إضافة ميزة، أم يتضمّن تغيير بنية؟
- سنوات بقاء النظام: هل سيُرحَّل أو يُوقَف خلال عام أو عامين، أم سيُستخدم خمس سنوات فما فوق؟
- أثر العطل: تشوّه في مظهر التقرير، أم خطأ في مبلغ الفاتورة أو عدد المخزون؟
| حجم التعديل | سنوات البقاء | أثر العطل | المستوى الموصى به |
|---|---|---|---|
| طفيف (بضعة أسطر / تغيير قيمة إعداد) | قصير (~عامان) | صغير (تشوّه عرض تقريباً) | اختبار التوصيف فقط. تثبيت المخرج المعني، التعديل، والانتهاء بالتحقّق من الفرق |
| طفيف إلى متوسّط | قصير | كبير (يتعلّق بالمبالغ أو المخزون) | اختبار التوصيف فقط بشكل أثقل. زيادة أنماط الإدخال بما يشمل الحدود |
| متوسّط (إضافة ميزة / تغيير منطق) | طويل (5 سنوات فأكثر) | صغير إلى متوسّط | اختبار التوصيف + إعداد اختبار وحدة مقتصر على محيط موضع التعديل (صنع نقطة تماس) |
| متوسّط إلى كبير | طويل | كبير | اختبار التوصيف + إعداد اختبار الوحدة + تقسيم وحدة الإصدار إلى أجزاء أصغر |
| كبير (يتطلّب تجديد البنية) | قصير | ─ | لا تلمس. لا تعدّل، اتبع تجنّباً تشغيليّاً، ووجّه الجهد نحو الترحيل أو الاستبدال |
| ─ (لا يوجد طلب تعديل أصلاً) | ─ | ─ | لا تلمس. لا تُجرِ إعادة هيكلة وقائيّة على كود عامل |
السطران الأخيران «لا تلمس» ليسا خياراً سلبيّاً، بل قراراً إيجابيّاً. الاستثمار في الجودة الداخليّة لنظام قصير البقاء لا يُسترد. تلك الساعات ينبغي توجيهها إلى قرار الترحيل المرتَّب في «جدول قرار إطالة عمر تطبيقات VB6 / Access وترحيلها» وإلى تصميم الوجهة.
وحتّى عند التقدّم إلى «إعداد اختبار الوحدة»، يلزم خط فاصل بين ما يُكتَب كاختبار وحدة وما يُترَك كاختبار تكامل (اختبار يستخدم قاعدة بيانات وملفّات حقيقيّة). هذا الفصل مرتَّب كجدول قرار في «كيف نرسم الحد بين اختبار الوحدة واختبار التكامل»، فراجعه أيضاً. وبما أنّ اختبار الوحدة يُفترَض أن يكون سريعاً ومعزولاً وقابلاً للتكرار،4 فمن الحكمة فصل اختبار التوصيف الذي يلامس قاعدة البيانات أو الملفّات في مشروع ووحدة تنفيذ منفصلة عن اختبار الوحدة.
6. قواعد التشغيل ── كي لا تنكسر شبكة الأمان
اختبار التوصيف يفقد معناه بسهولة إن أُخطئ تشغيله بعد كتابته. نقتصر على ثلاث قواعد دنيا.
6.1 لا تخلط إعادة الهيكلة بإضافة الميزات في الالتزام نفسه
إعادة الهيكلة تغيير يجعل الكود أسهل فهماً وصيانة دون تغيير السلوك.5 أي أنّ شرط النجاح فرق صفري مع Golden Master. أمّا شرط نجاح إضافة الميزة أو إصلاح العيب فهو ظهور الفرق المقصود فقط. إن خُلط هذان في التزام واحد، تعذّر عند ظهور فرق تحديد ما إذا كان «تغييراً مقصوداً» أو «كسراً».
| نوع التغيير | التعامل مع Golden Master | شرط النجاح |
|---|---|---|
| إعادة الهيكلة (تغيير البنية) | لا تُحدَّث | فرق صفري |
| إصلاح عيب / إضافة ميزة (تغيير السلوك) | تُحدَّث بعد مراجعة الفرق | الفرق المقصود فقط |
| صنع نقطة تماس (استخراج طريقة / حقن واجهة) | لا تُحدَّث | فرق صفري |
| تغيير قاعدة تطبيع القيمة المتوقّعة | تُعاد توليدها | يُذكَر سبب التغيير في رسالة الالتزام |
الأمر نفسه على وحدة الإصدار. «إصدار يقتصر على إعادة الهيكلة» يُفترَض ألّا يتغيّر فيه السلوك، فإن ظهر عطل أمكن الاشتباه فوراً بإعادة الهيكلة. إن خُلطا لم يعمل هذا الفصل.
6.2 تحديث القيمة المتوقّعة بترتيب «مراجعة الفرق ثم الاستبدال»
عند تغيير السلوك عمداً تُحدَّث النسخة المرجعيّة أيضاً. ثبّت الخطوات.
- توليد المخرج بعد التغيير، ومراجعة الفرق مع القيمة المتوقّعة الحاليّة بصريّاً
- التأكّد من أنّ الفرق يقتصر على التغيير المقصود فقط (إن تغيّر ولو سطر واحد غير مقصود، يُحقَّق فيه)
- استبدال ملف القيمة المتوقّعة بالمخرج الجديد، وإدراجه في التزام الكود نفسه ليبقى في السجل
الخطر تشغيل من نوع «استبدال القيمة المتوقّعة لتحويل الاختبار إلى أخضر لأنّه احمرّ». إن فُعل ذلك سُجّل التراجع كما هو باعتباره «صحيحاً»، وتوقّفت شبكة الأمان عن كونها شبكة أمان.
طريقة الحكم أسرع ما تكون بالنظر إلى فرق فعلي، فنورد مثالاً. لنفترض تعديلاً يضيف معدّل الضريبة المخفَّض 8% إلى سلع كانت بنسبة 10%، وظهر الفرق مع القيمة المتوقّعة كالتالي.
2026/06/30,A商事,事務用品, 10000, 1000, 11000
- 2026/06/30,A商事,飲料(軽減), 5000, 500, 5500
+ 2026/06/30,A商事,飲料(軽減), 5000, 400, 5400
2026/06/30,A商事,小計, 15000, 1500, 16500
- 2026/06/30,B工業,機械部品, 200000, 20000, 220000
+ 2026/06/30,B工業,機械部品, 200000, 20001, 220001
السطران العلويّان (تغيّر مبلغ الضريبة في سطر المعدّل المخفَّض من 500 إلى 400) تغيير مقصود. هما هدف التعديل نفسه، فيجوز تحديث القيمة المتوقّعة. أمّا السطران السفليّان، حيث انحرف مبلغ ضريبة B工業 التي لا علاقة لها بالمعدّل المخفَّض بريال واحد، فـتراجع. غالباً لمستَ دالّة مشتركة لمعالجة الكسور. إن استبدلت القيمة المتوقّعة هنا بـ«مجرّد ريال واحد»، ثُبّت هذا الانحراف لاحقاً باعتباره «سلوكاً صحيحاً».
قاعدة التشغيل تُلخَّص في جملة. إن لم تستطع شرح «لماذا تغيّر هذا السطر» لكل سطر في الفرق، فلا تحدّث القيمة المتوقّعة. إن وُجد سطر واحد لا يُشرح، حقّق حتّى يتّضح السبب. بالمناسبة في المثال أعلاه يمكن أيضاً ملاحظة أنّ سطر 小計 لم يُحدَّث (إن تغيّر سطر المعدّل المخفَّض ينبغي أن يتغيّر المجموع الفرعي). السطر الذي كان ينبغي أن يتغيّر ولم يتغيّر أيضاً هدف لمراجعة الفرق.
6.3 إعداد حد أدنى يعمل محلّيّاً حتّى بلا CI
حتّى في موقع بلا خادم CI يمكن البدء اليوم بالحد الأدنى التالي.
- إضافة مشروع اختبار واحد إلى الحل (يعمل MSTest / NUnit / xUnit جميعاً حتّى مع بقاء .NET Framework. طريقة الربط في القسم 3.4)
- وضع ملفّات القيمة المتوقّعة وبيانات الإدخال في مجلّد
TestData، وإدارة إصدارها مع الكود - جعل تشغيل الاختبار يدويّاً قبل الالتزام وعداً يلتزم به الفريق
- إدراج سطر واحد في دليل إجراءات الإصدار «تشغيل الاختبار والتأكّد من فرق صفري» حتّى لا يُنسى التحقّق من نتيجة التشغيل
وسيلة التشغيل ليست dotnet test وحدها. في حل تختلط فيه مشاريع csproj بالصيغة القديمة (غير نمط SDK) قد لا يعمل dotnet test كما يُتوقَّع. عندئذ خياران.
- مستكشف الاختبارات في Visual Studio. بعد البناء تُكتشَف الاختبارات تلقائيّاً وتُشغَّل من الواجهة. لأعضاء الفريق بلا خبرة اختبار هذا أسهل في الإدخال.
vstest.console.exe. أداة سطر أوامر تشغّل DLL اختبار مبنيّاً مباشرة، وتُستخدم من Developer Command Prompt.6
vstest.console.exe MyApp.Tests\bin\Debug\MyApp.Tests.dll /logger:trx
أيّها تستخدم بحسب البيئة. المهم وعد «تشغيل مرّة حتماً قبل الالتزام».
عند مطابقة نتيجة تشغيل الاختبار مع مخرج جانب التطبيق، كلّما كانت السجلات مُعدَّة تسارع التحقيق في السبب. ما ينبغي إبقاؤه في السجل مشروح في «الحد الأدنى من متطلّبات مسجّل ذاتي الصنع وقائمة تحقّق اختبار التكامل».
7. الخلاصة
- الكود القديم هو «كود بلا اختبارات»،1 والسبب الحقيقي لانكساره عند اللمس عدم وجود وسيلة للتأكّد من نتيجة التغيير. قبل الإصلاح ثبّت السلوك الحالي بالاختبار.
- اختبار التوصيف يسجّل «السلوك الحالي» لا «السلوك الصحيح». يمكن البدء بكود C# بسيط عبر أسلوب Golden Master (اختبار الموافقة / اختبار اللقطة) الذي يحفظ التقرير وCSV ونتيجة الحساب كما هي في ملف قيمة متوقّعة ويقارن الفروق.
- أوّل حاجز هو «جعل مشروع الاختبار قادراً على استدعاء كود EXE القائم». مشروع EXE يمكن الرجوع إليه كمرجع مشروع أيضاً، وإن أردت الاختبار مع بقاء
internalفأضفInternalsVisibleToسطراً واحداً إلى جانب المتن (القسم 3.4).2 - للبنى التي لا يُدخَل فيها اختبار، اصنع نقطة تماس (seam) بـ استخراج طريقة وحقن واجهة. لفّ اعتماديّة مثل
DateTime.Nowقاعدة وردت أيضاً في إرشادات اختبار الوحدة لدى Microsoft.43 - مدى التجهيز يُقرَّر بـ حجم التعديل × سنوات البقاء × أثر العطل. «اختبار التوصيف فقط» و«عدم اللمس» أيضاً قراران معتبَران.
- في التشغيل احرص على عدم خلط إعادة الهيكلة (شرط النجاح فرق صفري) وإضافة الميزة (شرط النجاح الفرق المقصود فقط)،5 وعلى مرور تحديث القيمة المتوقّعة عبر مراجعة الفرق دائماً. حتّى بلا 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, InternalsVisibleToAttribute Class. حول كون السمة تجعل أنواعاً وأعضاء لا تُرى عادة إلّا داخل التجميعة نفسها مرئيّة من تجميعة صديقة محدَّدة، وأنّ الهدف
internal/protected internal/private protectedولا يشملprivate، وأنّ التجميعة الحاليّة والتجميعة الصديقة يجب أن تكونا كلتيهما بلا توقيع أو كلتيهما باسم قوي، وأنّ حالة الاسم القوي تتطلّب المفتاح العام الكامل لا رمز المفتاح العام، ويُحصَل عليه بـsn -pوsn -tp. ↩ ↩2 ↩3 -
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. حول خصائص اختبار الوحدة الجيّد (fast / isolated / repeatable / self-checking / timely)، وطريقة إدخال نقطة تماس (seam) بلفّ اعتماديّة لا يمكن التحكّم بها مثل
DateTime.Nowداخل واجهة، وضرورة فصل الاعتماديّة على البنية التحتيّة عن اختبار الوحدة إلى اختبار التكامل. ↩ ↩2 ↩3 -
Microsoft Learn, Refactor code (Visual Studio). حول تعريف إعادة الهيكلة بأنّها عمليّة تغيير الكود لجعله أسهل صيانة وفهماً وتوسيعاً دون تغيير السلوك. ↩ ↩2
-
Microsoft Learn, VSTest.Console.exe command-line options. حول كون VSTest.Console.exe أداة سطر أوامر لتشغيل الاختبارات، وإمكان تشغيل ملف اختبار (DLL) بتحديده مباشرة، وإمكان الاستخدام من Developer Command Prompt. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
إلى متى ستستمر تطبيقات VB6 في العمل ── وضع دعم بيئة التشغيل والمسار العملي للترحيل إلى .NET
إلى متى تستمر تطبيقات VB6 في العمل؟ نرتّب وضع بيئة التشغيل المشمولة حتى في Windows 11 وانتهاء دعم بيئة التطوير، وجدول القرار بين إعادة ال...
إدارة إصدارات مخطّط قاعدة بيانات تطبيقات الأعمال ── ممارسات الترحيل لمنع «اختلاف قاعدة البيانات من عميل لآخر»
دليل عمليّ لإدارة إصدارات مخطّط قاعدة بيانات تطبيقات الأعمال الموزَّعة على عملاء متعدّدين. نشرح 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: أهو «تغيير مقصود» أم «كسر». الآمن التحقّق منفصلَين: فرق صفري في التزام إعادة الهيكلة، وفرق مقصود فقط في التزام إضافة الميزة.
- متى تُحدَّث النسخة المرجعيّة (ملف القيمة المتوقّعة)؟
- فقط لحظة تغيير السلوك عمداً، أي عند التزام إضافة ميزة أو إصلاح عيب. عند التحديث راجع فرق المخرج قبل التغيير وبعده بصريّاً، وتأكّد أنّه لا يحتوي إلّا على التغيير المقصود، ثم استبدل القيمة المتوقّعة بالمخرج الجديد. إن استُبدلت القيمة المتوقّعة آليّاً لأنّ الاختبار احمرّ، دُمج التراجع (تغيّر سلوك غير مقصود) كما هو باعتباره «صحيحاً»، وفقدت شبكة الأمان معناها.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.