انتقل إلى المحتوى
كل الملاحظات

3 د للقراءة

بيانات اختبار تقول الحقيقة

الـ fixtures تكذب بالإغفال. mongoose-test-factory يقرأ مخطّطك ويولّد بيانات تحترمه. كيف يقرّر ما يولّده، والأنماط الثلاثة التي أستعملها كل يوم.

كل مشروع Mongoose انضممت إليه كان فيه مجلد fixtures/، وكل واحد من تلك المجلدات كان خاطئًا بهدوء. حقل أُضيف إلى المخطّط ولم يُضَف إلى JSON. تعداد (enum) اكتسب قيمة لم يختبرها أحد. علامة required ظهرت فتوقّف نصف الـ fixtures عن الحفظ، فوسم أحدهم الاختبار بـ skip ومضى.

الـ fixtures لقطة لما كان عليه المخطّط يوم كُتبت. أما المخطّط فهو ما تعتقده الشيفرة اليوم. وُجد mongoose-test-factory ليغلق تلك الفجوة: يقرأ المخطّط ويولّد بيانات صالحة بحسبه، الآن.

#ابدأ من المخطّط

الحزمة إضافة (plugin). طبّقها، غلّف النموذج، فيحصل النموذج على factory().

user.model.tstypescript
import mongoose, { Schema } from 'mongoose'
import mongooseTestFactory, { withFactory } from 'mongoose-test-factory'

const userSchema = new Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  age: { type: Number, min: 18, max: 120 },
  isActive: { type: Boolean, default: true },
})

userSchema.plugin(mongooseTestFactory)
export const User = withFactory(mongoose.model('User', userSchema))
typescript
const user = User.factory().build()
// { name: 'John Doe', email: 'john.doe@example.com', age: 28, isActive: true }

لم يُضبط شيء. مشت الإضافة على المخطّط، فرأت سلسلة إلزامية باسم name، وسلسلة فريدة باسم email، ورقمًا محصورًا بين 18 و120، وولّدت وفق ذلك. غيّر المخطّط فيتغيّر التشغيل التالي معه.

#كيف يقرّر ما يولّده

ثلاث طبقات، بترتيب الأولوية.

  1. factoryType صريح على الحقل يفوز. نحو أربعين نوعًا مدمجًا: email وphone وprice وslug وuuid وbirthdate وtags وغيرها.
  2. اسم الحقل. userEmail وcontactEmail وemail كلها تبدو بريدًا إلكترونيًا. price وcost وamount تبدو مالًا. createdAt يبدو طابعًا زمنيًا.
  3. النوع ومُحقِّقاته. Number مع min وmax يبقى داخل الحدود. String مع enum تختار منه. required يُحترم دائمًا.

حين يكون الاسم ملتبسًا، قُل ذلك في المخطّط:

typescript
const productSchema = new Schema({
  name: { type: String, factoryType: 'title' },
  vendor: { type: String, factoryType: 'company' },
  price: { type: Number, factoryType: 'price' },
  isActive: { type: Boolean, factoryType: 'active' }, // true about 80% of the time
  website: { type: String, factoryType: 'url' },
})

#ثلاثة أنماط، ثلاثة أنواع من الاختبارات

الدالةتُعيدتمسّ قاعدة البياناتاستعملها في
build()كائنات عاديةلااختبارات الوحدة، أجسام الطلبات
make()نُسَخ Mongooseلاالحقول الافتراضية، الدوال، الخطافات
create()مستندات محفوظةنعماختبارات التكامل
typescript
const body = User.factory().build() // fast, no connection needed
const doc = User.factory().make() // an instance, not persisted
const saved = await User.factory(50).create() // fifty real rows

معظم اختبارات الوحدة عندي لا تفتح اتصالًا أبدًا. build() سريع بما يكفي لأتوقّف كليًا عن تخزين بيانات الاختبار مؤقتًا.

#التجاوزات والعلاقات

القيمة المولّدة نقطة بداية. وما يهمّ الاختبار حقًا، حدّده صراحةً.

typescript
const admin = User.factory().with({ name: 'Admin', role: 'admin' }).build()

const author = await User.factory().create()
const posts = await Post.factory(5).with({ author: author._id }).create()

المثال الثاني هو النمط الذي أستعمله أكثر: أصل واحد حقيقي، وN من الأبناء المولّدين يشيرون إليه. يملأ المصنع كل ما لم أذكره، فتُقرأ التوكيدات كنيّة لا كتحضير.

#ما الذي لن يفعله

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

هل أفادك هذا؟

النقاش

تبني شيئًا من هذا القبيل؟

أخبرني بما تعمل عليه. أردّ في غضون يوم.

راسلني