التهيئة في الباني.
يقوم فئة الاختبار بتهيئة حقول التجهيز (fixture fields) الخاصة به في الباني (constructor) بدلاً من خطاف التهيئة المخصص لإطار العمل (setUp / @BeforeEach / TestInitialize)، مما يؤدي إلى تجاوز دورة حياة الاختبار.
##Signs and Symptoms
تحتوي فئة الاختبار على باني صريح يبني النظام تحت الاختبار، أو الكائنات الوهمية (mocks)، أو الحقول المشتركة — وهو العمل الذي ينتمي إلى خطاف التهيئة الخاص بإطار العمل (setUp() / @Before / @BeforeEach في JUnit، أو [TestInitialize] في MSTest، أو beforeEach في Jest/Vitest).
العلامات الدالة:
- تعلن فئة الاختبار عن باني يسند القيم لحقول ثابتة (
final) أو حقول مثيل (instance fields). - تعيش تهيئة التجهيز خارج دورة الحياة التي يديرها إطار العمل بالفعل — لذلك يتم تجاوز
@BeforeEach/@AfterEach، وتهيئة الفئة الأساسية، وإعادة التهيئة لكل اختبار بصمت، أو تشغيلها بترتيب محير مقارنة بالباني. - التهيئة غير المتزامنة مستحيلة: لا يمكنك استخدام
awaitداخل الباني، مما يجبر عمل التجهيز غير المتزامن على التواجد في المكان الخاطئ أو تكراره.
النموذج النموذجي في JUnit (من كتالوج روائح الاختبار):
public class TagEncodingTest extends BrambleTestCase {
private final CryptoComponent crypto;
private final SecretKey tagKey;
public TagEncodingTest() { // رائحة: التهيئة في الباني
crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
tagKey = TestUtils.getSecretKey();
}
}
نظير JS/TS — بناء التجهيز مرة واحدة على مستوى نطاق describe أو نطاق الوحدة (مكافئ الباني) بدلاً من بنائه لكل اختبار:
describe('Cart', () => {
const cart = new Cart(); // يُبنى مرة واحدة، ويسرب الحالة عبر الاختبارات
it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
it('starts empty', () => { expect(cart.size).toBe(0); }); // يفشل: يرى التفاحة (apple)
});
##Reasons for the Problem
لماذا يحدث ذلك
- يلجأ المطورون غير المعتادين على الغرض من خطاف التهيئة (setup hook) إلى ميزة اللغة التي يعرفونها بالفعل — الباني (constructor) — لتهيئة الحقول.
- يبدو الأمر أكثر نظافة: الحقول الثابتة (
final) بالإضافة إلى الباني تعطي شعوراً بالتوافق مع اللغة، وقد يقوم محرّر الأكواد (IDE) بإنشاء الباني نيابةً عنك. - التقليد الأعمى (Cargo-culting) من فئات الإنتاج، حيث تكون التهيئة في الباني هي النمط الصحيح.
لماذا يضر ذلك
- يتجاوز دورة حياة الاختبار. تضمن أطر العمل حالة جديدة لكل اختبار عبر خطاف التهيئة وزوج
@BeforeEach/@AfterEach(أوbeforeEach/afterEach). المنطق المخفي في الباني يقع خارج هذا العقد، لذا قد لا تنطبق عمليات التنظيف، وتهيئة الفئات الأساسية، وضمانات الترتيب كما هو متوقع. - تسرب الحالة وعدم الاستقرار (Flakiness). في أطر العمل التي تعيد استخدام نفس المثيل (أو عند تخزين التجهيز في متغير مشترك أو على مستوى نطاق
describe)، تتسرب التعديلات من اختبار إلى آخر. تنجح الاختبارات أو تفشل اعتماداً على ترتيب التنفيذ — مما يؤدي إلى ثقة زائفة وفشل متقطع. - لا توجد تهيئة غير متزامنة. لا يمكن للباني استخدام
await. أي تهيئة تحتاج إلى إدخال/إخراج (I/O)، أو اتصال بقاعدة البيانات، أو خادم بدأ تشغيله لا يمكن أن توجد هناك بشكل نظيف؛ ويتحايل الناس على ذلك باستخدام استدعاءات حاجزة (blocking calls) أو كود مكرر لكل اختبار. - سهولة القراءة / الاتساق. يتوقع المراجعون والأدوات وجود التجهيزات في الخطاف التقليدي. يؤدي تقسيم التهيئة بين الباني وطريقة التهيئة إلى جعل التجهيز الحقيقي صعب العثور عليه وسهل الوقوع في الخطأ.
تنبيه — يعتمد هذا على إطار العمل. هذه ليست قاعدة عامة. في xUnit.net، يعتبر الباني هو التهيئة المعتادة لكل اختبار (مقروناً بـ IDisposable للتفكيك)، كما أن MSTest توفر قاعدة اختيارية معاكسة (MSTEST0020) تفضل البواني على [TestInitialize]. تعامل مع "التهيئة في الباني" كرائحة تحديداً عندما يوفر إطار عملك خطاف تهيئة مخصصاً (JUnit، أو MSTest الكلاسيكي، أو NUnit) وتقوم أنت بتجاوزه.
##Treatment
انقل تهيئة الحقول خارج الباني وإلى خطاف التهيئة الخاص بإطار العمل، ويفضل استخدام حالة جديدة لكل اختبار بدلاً من المثيلات المشتركة.
- احذف باني فئة الاختبار؛ صرّح بالحقول وأسندها في خطاف التهيئة (
@BeforeEach/setUp()لـ JUnit، أو[TestInitialize]لـ MSTest، أوbeforeEachلـ Jest/Vitest). - إذا كانت التهيئة غير متزامنة، فهذا إلزامي — حيث يمكن للخطاف استخدام
await؛ بينما لا يمكن للباني ذلك. - في JS/TS، ابنِ التجهيز داخل
beforeEach(إعادة تعيين متغيرlet) بدلاً من بنائه مرة واحدة في نطاقdescribeأو نطاق الوحدة، حتى يحصل كل اختبار على كائن نظيف. - حافظ على تماثل التفكيك: اربط
@BeforeEachمع@AfterEach(أو، في نمط الباني الخاص بـ xUnit.net، قم بتنفيذ واجهةIDisposable).
قبل ← بعد (JUnit):
// قبل
public class TagEncodingTest {
private final CryptoComponent crypto = new CryptoComponentImpl(...);
}
// بعد
public class TagEncodingTest {
private CryptoComponent crypto;
@BeforeEach
void setUp() {
crypto = new CryptoComponentImpl(...);
}
}
// بعد
قبل ← بعد (Jest/Vitest):
describe('Cart', () => {
let cart: Cart;
beforeEach(() => { cart = new Cart(); }); // fresh per test
it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
it('starts empty', () => { expect(cart.size).toBe(0); });
});
إذا كان التجهيز غير قابل للتغيير (immutable) ومكلفاً بالفعل (وإطار العمل يدعم ذلك)، فإن استخدام خطاف لمرة واحدة مثل @BeforeAll/beforeAll مقبول — ولكن فقط للحالة المشتركة للقراءة فقط، وليس للكائنات التي تعدلها الاختبارات.
##Detected by
- tsDetect (كاشف روائح الاختبار) التهيئة في الباني
- محللات MSTest Roslyn MSTEST0019