بيانات الاختبار والتجهيزات والخطافات
يحتاج الاختبار المؤتمت إلى نقطة بداية معروفة: عميل موجود، وطلب فيه منتج واحد، وصفحة مفتوحة في المتصفح. بيانات الاختبار (Test Data) هي القيم التي يستخدمها الاختبار؛ والتجهيزة (Fixture) إعداد قابل لإعادة الاستخدام يُنشئ الحالة التي يحتاجها الاختبار ويسلّمها له ثم يزيلها بعده؛ والخطافات (Hooks) دوال يستدعيها مشغّل الاختبارات تلقائياً قبل الاختبارات أو بعدها، مثل beforeEach وafterEach. فاختبار الدفع مثلاً يتسلّم عميلاً جديداً من تجهيزة، ويفتح السلة في خطاف beforeEach، ثم تحذف التجهيزة العميل عند انتهاء الاختبار، سواء نجح أم فشل.
أما الاختبارات ذات المعاملات (Parameterized)، أو المعتمدة على البيانات (Data-driven)، فتحل المشكلة المقابلة: جسم اختبار واحد يعمل مرة لكل صف في جدول بيانات، فتصبح ستة رموز خصم ستة اختبارات في التقرير دون ست نسخ من الشيفرة. تناول درس بيانات الاختبار أيّ القيم تختار؛ وهذا الدرس يتناول أين تعيش هذه القيم في الاختبارات المؤتمتة، وكيف تُنشأ وكيف تُنظَّف.
لماذا يهم
كثير من حزم الاختبارات المؤتمتة غير الموثوقة تفشل بسبب الحالة لا بسبب المنتج. اختبار ينجح منفرداً ويفشل في التشغيل الكامل لأن اختباراً سابقاً غيّر الحساب المشترك؛ وتشغيل يفشل يوم الاثنين لأن القسيمة التي يعتمد عليها الاختبار انتهت صلاحيتها؛ وطلبات متبقية من آلاف التشغيلات تُبطئ صفحة قائمة حتى تتجاوز اختباراتها المهلة. فطريقة الإعداد والتنظيف هي ما يقرر هل تعني النتيجة الحمراء عيباً أم مجرد ضجيج، والضجيج يعلّم الفريق تجاهل النتائج الحمراء.
والإعداد يحدد كذلك كلفة الصيانة. فإذا تغيّر مسار تسجيل الدخول، فالتجهيزة مكان واحد يُعدَّل، أما عشرون كتلة إعداد منسوخة فعشرون مكاناً. والاختبارات المعتمدة على البيانات تتيح إضافة حالة بإضافة صف، والجدول المقروء يتيح لمراجِع أو لمالك المنتج فحص الحالات دون قراءة الشيفرة.
أين تعيش بيانات الاختبار في الاختبار المؤتمت
ينفّذ مشغّل الاختبارات الاختبارات دون إشراف، وبأي ترتيب، وغالباً على التوازي، لذا يجب أن تأتي كل قيمة يعتمد عليها الاختبار من مكان يتحكم فيه الاختبار. والاختبار الذي يعتمد بصمت على سجل أنشأه شخص يدوياً في بيئة مشتركة سيفشل يوم يُعدَّل ذلك السجل أو يُحذف، وسيبدو فشله كأنه عيب في المنتج.
تنطبق قاعدتان أينما كانت البيانات. الاختبار يملك البيانات التي يغيّرها: فلا يُشارَك إلا ما هو للقراءة فقط، مثل قائمة الدول أو كتالوج منتجات لا يعدّله أي اختبار. والحقل الذي يجب أن يكون فريداً يأخذ قيمة فريدة في كل تشغيل، مثل sara+${runId}@example.com، حتى لا يفشل تشغيل ثانٍ أو عامل (Worker) موازٍ برسالة «موجود مسبقاً» (already exists).
أما البيانات نفسها فيمكن أن تعيش في أربعة أماكن؛ اختر بحسب ما يحتاج قارئ الاختبار أن يراه.
- داخل الاختبار نفسه: القيمة أو القيمتان اللتان تشرحان الاختبار، مثل
quantity: 0في اختبار عنوانه «يرفض الكمية صفراً». أبقِ القيمة المهمة ظاهرة بجوار التأكيد. - دالة بانية أو دالة مصنع (Factory):
makeCustomer({ country: "JO" })تُعيد سجلاً صالحاً كاملاً بقيم افتراضية معقولة، ويغيّر الاختبار الحقل الذي يخصّه فقط. وإذا ظهر حقل إلزامي جديد، عدّلت المصنع مرة واحدة. - ملف بيانات: جدول CSV أو JSON في المستودع بجوار الاختبارات، حين تشترك حالات كثيرة في جسم اختبار واحد. ويُراجَع ويُدار إصداره كالشيفرة تماماً.
- حالة تُنشأ قبل الاختبار: سجلات يجب أن تكون موجودة، تُنشأ عبر الواجهة البرمجية (API) أو سكربت زرع (Seed) لا بالنقر عبر الشاشات، لأن النقر أبطأ ويفشل لأسباب لا تخص الاختبار.
الخطافات: شيفرة يستدعيها المشغّل قبل الاختبارات وبعدها
توفر معظم المشغّلات أربعة خطافات: beforeAll يعمل مرة واحدة قبل أول اختبار في الملف أو المجموعة، وbeforeEach قبل كل اختبار، وafterEach بعد كل اختبار، وafterAll مرة واحدة بعد آخر اختبار. يستخدم Jest وVitest هذه الأسماء بعينها؛ ويكتبها Playwright Test بصيغة test.beforeEach وما يشبهها؛ ويستخدم JUnit 5 التعليقات التوضيحية (Annotations) @BeforeAll و@BeforeEach و@AfterEach و@AfterAll. ويدعم pytest دوال إعداد وتفكيك بسيطة أيضاً، لكن أداته المعتادة هي التجهيزة، ونشرحها في القسم التالي.
الترتيب لكل اختبار هو: خطافات beforeEach، ثم جسم الاختبار، ثم خطافات afterEach. وفي المجموعات المتداخلة يعمل beforeEach الخاص بالمجموعة الخارجية قبل الخاص بالداخلية. والخاصية المهمة أن الخطافات اللاحقة تعمل حتى عندما يفشل الاختبار. فالتأكيد الفاشل يوقف جسم الاختبار عند ذلك السطر، لذا لا يعمل أبداً التنظيف المكتوب في آخر أسطر الجسم، بينما يعمل التنظيف الموضوع في afterEach.
استخدم beforeEach للحالة التي تغيّرها الاختبارات، ليبدأ كل اختبار من جديد. واستخدم beforeAll فقط للإعداد المكلف الذي تقرؤه الاختبارات ولا تغيّره أبداً، مثل تشغيل خادم بديل (Stub)؛ فالبيانات القابلة للتغيير التي تُنشأ مرة وتغيّرها عدة اختبارات تجعل النتيجة معتمدة على ترتيب تشغيل الاختبارات. وفي Playwright Test يعمل beforeAll مرة لكل عملية عامل (Worker Process)، ويعمل مجدداً في العامل الجديد الذي يحل محل سابقه بعد فشل اختبار، لذا يجب أن يكون آمناً إذا تكرر تشغيله.
import { test, expect } from '@playwright/test';
test.describe('saved addresses', () => {
const created: string[] = []; // ids of addresses these tests created
test.beforeEach(async ({ page }) => {
await page.goto('/account/addresses'); // every test starts on the same page
});
test.afterEach(async ({ request }) => { // request: an HTTP client for the API
for (const id of created) { // runs even when the test failed
await request.delete(`/api/addresses/${id}`);
}
created.length = 0;
});
test('lists an address added through the API', async ({ page, request }) => {
const res = await request.post('/api/addresses', { data: { city: 'Riyadh' } });
created.push((await res.json()).id);
await page.reload();
await expect(page.getByText('Riyadh')).toBeVisible();
});
});التجهيزات: إعداد يطلبه الاختبار باسمه
تجمع التجهيزة ثلاثة أشياء: الإعداد (Setup)، والقيمة التي يتسلّمها الاختبار، والتفكيك (Teardown). ويطلب الاختبار التجهيزة باسمها بدلاً من أن يستدعي شيفرة الإعداد بنفسه. في Playwright Test تُعدّ page (صفحة في المتصفح) وrequest (عميل HTTP لاستدعاء الواجهة البرمجية مباشرةً) تجهيزات مدمجة، وتضيف تجهيزاتك بـtest.extend: الشيفرة قبل await use(value) إعداد، ويعمل الاختبار أثناء use، والشيفرة بعدها تفكيك يعمل سواء نجح الاختبار أم فشل. وفي pytest التجهيزة دالة موسومة بـ@pytest.fixture يطلبها الاختبار بذكر اسمها معاملاً له؛ وهي تسلّم القيمة بـyield، والشيفرة بعد yield هي التفكيك.
مقارنةً بالخطافات، تعمل التجهيزات عند الطلب: لا يدفع كلفة إنشاء عميل إلا الاختبار الذي يطلب customer. وهي قابلة للتركيب: تجهيزة order يمكن أن تطلب تجهيزة customer، ويجري التفكيك بالترتيب المعاكس، فيُحذف الطلب قبل عميله. ولها نطاق (Scope): لكل اختبار افتراضياً، أو مشتركة على نطاق أوسع، لكل عامل في Playwright أو لكل وحدة برمجية (Module) أو جلسة في pytest، للأشياء المكلفة التي تقرؤها الاختبارات فقط. وفي Playwright Test تُعَدّ التجهيزة افتراضياً عند الحاجة (Lazily)، قبيل أول خطاف أو اختبار يطلبها: فـpage التي يستخدمها خطاف beforeEach تكون جاهزة قبل تشغيل ذلك الخطاف، أما customer الذي لا يطلبه إلا جسم الاختبار فيُنشأ بعد خطافات beforeEach. وتُفكَّك التجهيزات ذات نطاق الاختبار بعد خطافات afterEach.
أبقِ القيم المهمة ظاهرة. يجب أن تُخفي التجهيزة كيف يُنشأ العميل، لا نوع العميل الذي يحتاجه الاختبار؛ فإذا اعتمد الاختبار على تفصيل مثل دولة العميل، فمرّره صراحةً أو أنشئ تجهيزة منفصلة باسم واضح.
- 1الخطاف
beforeAllقبل أول اختبار في الملف أو المجموعة، مرة لكل عامل؛ فقط لإعداد تقرؤه الاختبارات ولا تغيّرهمرة واحدة - 2إعداد التجهيزة
pageتُعَدّ أولاً لأن خطافbeforeEachيطلبpage - 3الخطاف
beforeEachيفتح الصفحة التي يبدأ منها كل اختبار - 4إعداد التجهيزة
customerلا يطلبها إلا جسم الاختبار، فتُعَدّ الآن: الشيفرة قبلawait use()تُنشئ عميلاً جديداً عبر API - 5جسم الاختبارينفّذ الخطوات ويتحقق، مستخدماً العميل الذي سلّمته التجهيزة
فرع: يفشل تأكيد: يتوقف الجسم عند ذلك السطر، وتعمل الخطوتان 6 و7 مع ذلكفشل
- 6الخطاف
afterEachيعمل سواء نجح الاختبار أم فشل، مثلاً لحذف المعرّفات التي سجّلها الاختبار - 7تفكيك التجهيزاتبعكس ترتيب الإعداد: الشيفرة بعد
use()تحذف العميل، ثم تُغلقpage - 8الخطاف
afterAllبعد آخر اختبار في الملف أو المجموعةمرة واحدة
طوال الخطوات 2–7 يتكرر لكل اختبار
الترتيب كما توثّقه Playwright Test. ويشغّل Jest وVitest وJUnit الخطافات الأربعة نفسها؛ ويبني pytest هذه الخطوات من التجهيزات.
// fixtures.ts
import { test as base, expect } from '@playwright/test';
import { randomUUID } from 'node:crypto';
type Customer = { id: string; email: string };
export const test = base.extend<{ customer: Customer }>({
customer: async ({ request }, use) => {
// setup: a fresh customer for this test only
const email = `sara+${randomUUID()}@example.com`;
const res = await request.post('/api/customers', { data: { email } });
const customer: Customer = await res.json();
await use(customer); // the test runs here
// teardown: runs after the test, passed or failed
await request.delete(`/api/customers/${customer.id}`);
},
});
export { expect };
// orders.spec.ts
import { test, expect } from './fixtures';
test('a new customer has no orders', async ({ request, customer }) => {
const res = await request.get(`/api/customers/${customer.id}/orders`);
expect(await res.json()).toEqual([]);
});التنظيف: اترك البيئة كما وجدتها
البيانات المتبقية ليست بلا ضرر. فهي تملأ القوائم ونتائج البحث التي تتحقق منها اختبارات أخرى، وتصطدم بقيود التفرّد، وتربك المختبِرين اليدويين في بيئة الاختبار المشتركة. وكل اختبار يُنشئ شيئاً يحتاج إلى خطة لإزالته، أو إلى بيئة تُرمى بعد الاستخدام.
- ضع التنظيف (Cleanup) في خطاف لاحق أو في تفكيك التجهيزة، ولا تضعه أبداً في نهاية جسم الاختبار وحدها.
- احذف بالضبط ما أنشأه الاختبار، بالمعرّف. ولا تنظّف أبداً باستعلام واسع مثل «احذف كل عميل ينتهي بريده بـexample.com»: ففي البيئة المشتركة يزيل ذلك بيانات الآخرين وبيانات التشغيلات المتوازية.
- افصل خطوات الإعداد، حتى لا تؤدي خطوة فاشلة إلى تخطي تفكيك الخطوات التي سبقتها؛ ولهذا السبب توصي وثائق pytest بإجراء واحد يغيّر الحالة في كل تجهيزة.
- اجعل التفكيك متسامحاً مع سجل محذوف أصلاً، كأن يكون الاختبار قد حذفه بنفسه، لكن أبلغ عن أي فشل آخر في التنظيف بدلاً من إخفائه.
- فضّل البيانات التي تزول وحدها حيث يسمح الإعداد بذلك: معاملة قاعدة بيانات (Transaction) يُتراجع عنها (Rollback) بعد كل اختبار، في اختبارات التكامل التي تشارك الشيفرةَ المختبَرة اتصالَ قاعدة البيانات نفسه؛ أو حاوية جديدة أو بيئة يُعاد ضبطها في كل تشغيل؛ أو مستأجر (Tenant) منفصل لكل تشغيل.
- ميّز البيانات المنشأة ببادئة التشغيل، مثل
AT-20261002-0412-، لتتمكن مهمة مجدولة من العثور على ما تركته التشغيلات المتعطلة وإزالته.
مثال تطبيقي: اختبار واحد وجدول من رموز الخصم
المتطلب: «الرمز SAVE10 يمنح خصم 10% على الطلبات بقيمة 200.00 ريال فأكثر، ويُرفض على ما دونها برسالة الحد الأدنى للطلب. والرموز لا تميّز بين الأحرف الكبيرة والصغيرة. والرمز المنتهي أو غير المعروف يُبقي الإجمالي كما هو ويعرض خطأ». كتابة ستة اختبارات شبه متطابقة تنسخ الخطوات نفسها ست مرات. أما الاختبار المعتمد على البيانات فيُبقي جسم اختبار واحداً وينقل الحالات إلى جدول؛ ويصف مسرد ISTQB الاختبار المعتمد على البيانات بأنه تقنية لكتابة السكربتات تحفظ فيها ملفاتُ البيانات بياناتِ الاختبار والنتائجَ المتوقعة.
كل المشغّلات الرئيسية تدعم ذلك. في Jest وVitest ينشئ test.each(rows)("$case", fn) اختباراً لكل صف ويضع قيمة case في عنوانه. وفي pytest يفعل @pytest.mark.parametrize("row", rows) الشيء نفسه، ويسمّي pytest.param(row, id=...) كل حالة. وفي JUnit 5 يقرأ @ParameterizedTest مع @CsvFileSource(resources = "/discount-cases.csv", numLinesToSkip = 1) الصفوف من ملف CSV ويتخطى سطر العناوين. أما Playwright Test فلا يوفر test.each؛ وتنشئ وثائقه test() لكل صف داخل حلقة، ويجب أن يكون كل عنوان فريداً.
الجدول أدناه يحوي المدخلات والنتائج المتوقعة فقط، وكل إجمالي متوقع محسوب يدوياً من المتطلب. ويقرأ الاختبار الملف ويشغّل كل صف بسلة جديدة من تجهيزة.
- أعطِ كل صف عموداً للاسم. عندها يقول التقرير «فشلت حالة expired code» بدلاً من «فشل الصف 5».
- اكتب القيم كما يعرضها النظام تماماً:
200.00لا200. وفي CSV ينبغي وضع القيمة التي تحوي فاصلة أو علامة اقتباس مزدوجة أو فاصل أسطر بين علامتي اقتباس مزدوجتين، وتُكتب علامة الاقتباس المزدوجة داخلها مرتين (RFC 4180). - المأزق الأول، الجدول العملاق: ملف من 200 صف بأعمدة لكل ميزة لا يمكن مراجعته، وكل تغيير يمسّه. اجعل لكل قاعدة جدولها، صغيراً بما يكفي ليُقرأ في دقيقة، واختر الصفوف بتقنيات تصميم الاختبار مثل تحليل القيم الحدّية بدلاً من تكديسها.
- المأزق الثاني، منطق الاختبار المخبأ في البيانات: أعمدة مثل
click_apply_twiceأوskip_loginتحوّل الجدول إلى برنامج لا يختبره أحد. فإذا احتاجت الصفوف خطوات مختلفة، فهي اختبارات مختلفة. والأمر نفسه للنتائج المتوقعة التي يحسبها الاختبار بمعادلة المنتج نفسها: عندها يختبئ عيب المعادلة في الطرفين.
data/discount-cases.csv
case,code,cart_total,expected_total,error
valid code,SAVE10,250.00,225.00,
lower-case code,save10,200.00,180.00,
minimum order exactly,SAVE10,200.00,180.00,
just below minimum,SAVE10,199.99,199.99,MIN_ORDER
expired code,OLD5,200.00,200.00,EXPIRED
unknown code,NOPE10,200.00,200.00,NOT_FOUND
test_discounts.py (pytest: one test body, six reported tests)
import csv
from decimal import Decimal
import pytest
def load_cases(path):
with open(path, newline="", encoding="utf-8") as f:
return [pytest.param(row, id=row["case"]) for row in csv.DictReader(f)]
@pytest.mark.parametrize("row", load_cases("data/discount-cases.csv"))
def test_discount_code(cart, row): # cart: a fixture, fresh per test
cart.set_total(Decimal(row["cart_total"]))
result = cart.apply_code(row["code"])
assert result.total == Decimal(row["expected_total"])
assert result.error == (row["error"] or None)أخطاء شائعة
- كتابة التنظيف في آخر أسطر جسم الاختبار، حيث يتخطاه أول تأكيد فاشل.
- إنشاء البيانات مرة واحدة في
beforeAllوترك عدة اختبارات تغيّرها، فينجح الاختبار أو يفشل بحسب الاختبارات التي سبقته. - الاعتماد على سجلات أُنشئت يدوياً في بيئة مشتركة، مثل «العميل 1001»، يستطيع أي شخص تعديلها أو حذفها.
- استخدام قيمة ثابتة مثل
test@example.comلحقل يجب أن يكون فريداً، فيفشل التشغيل الثاني أو العامل الموازي برسالة «موجود مسبقاً». - إخفاء القيم المهمة داخل تجهيزة، فيرى القارئ
test("works", async ({ setup }) => …)ولا يعرف ما يحتاجه الاختبار ولا ما يتحقق منه. - توليد قيم عشوائية للمدخلات التي تحدد النتيجة المتوقعة دون تسجيلها، فلا يمكن إعادة إنتاج الإخفاق.
من منظور ضمان الجودة
عندما تراجع اختباراً مؤتمتاً، كتبته أنت أو كتبه مطور، اسأل أربعة أسئلة عن بياناته: من أين تأتي؟ ومن غيره يستطيع تغييرها؟ وماذا يحدث لها بعد الاختبار؟ وهل يمكن تشغيل الاختبار منفرداً، ومرتين متتاليتين، وعلى التوازي مع نفسه؟ والاختبار الذي لا يجتاز أحد هذه الأسئلة سيفشل يوماً لسبب لا علاقة له بالمنتج. واتفق مع المطورين مبكراً على طريقة إنشاء الاختبارات للبيانات وإزالتها: نقاط نهاية API خاصة بالاختبار أو سكربتات زرع لا تُنشر أبداً في بيئة الإنتاج، وأعراف القيم الفريدة، وعدد مرات إعادة ضبط البيئات المشتركة.
جداول الاختبارات المعتمدة على البيانات هي حيث يضيف المختبِر أكبر قيمة. فالصفوف تأتي من تقنيات تصميم الاختبار مثل تقسيم التكافؤ وتحليل القيم الحدّية وجداول القرار، وكلٌّ منها يحوّل القاعدة إلى صفوف قليلة ذات معنى. ويستطيع مالك المنتج مراجعة جدول حسن التسمية دون قراءة الشيفرة. وإذا فشل صف، فيجب أن يذكر تقرير الخطأ الصف وبياناته والمعرّفات التي أنشأتها التجهيزات، ليعيد المطور إنتاج الحالة نفسها بالضبط.
تمرين عملي
أعِد هيكلة اختبار قسائم هشّ
كُتب الاختبار أدناه على عجل، ويفشل في CI مرة في الأسبوع تقريباً. اقرأه واكتب كل مشكلة في بياناته وإعداده وتنظيفه. ثم صف التجهيزات والخطافات وجدول البيانات التي ستستخدمها بدلاً منه، وكم اختباراً يجب أن يظهر في تقرير التشغيل.
test('coupon tests', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('qa.shared@example.com');
await page.getByLabel('Password').fill(process.env.SHARED_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
const res = await page.request.post('/api/orders', { data: { total: '200.00' } });
const order = await res.json();
for (const c of [
{ code: 'SAVE10', expected: '180.00', applyTwice: false },
{ code: 'save10', expected: '180.00', applyTwice: true },
{ code: 'OLD5', expected: 'error', applyTwice: false },
]) {
await page.goto(`/orders/${order.id}`);
await page.getByLabel('Coupon').fill(c.code);
await page.getByRole('button', { name: 'Apply' }).click();
if (c.applyTwice) await page.getByRole('button', { name: 'Apply' }).click();
if (c.expected === 'error') await expect(page.getByRole('alert')).toBeVisible();
else await expect(page.getByTestId('total')).toHaveText(c.expected);
}
await page.request.delete(`/api/orders/${order.id}`);
});- جد البيانات التي لا يملكها الاختبار، ومن غيره يستطيع تغييرها.
- جد التنظيف الذي يمكن أن يُتخطى، والحالة التي تتسرب من حالة إلى التالية.
- جد المنطق المخبأ في البيانات.
- صمّم البديل: التجهيزات مع تفكيكها، وأي خطاف، والجدول بأعمدته وصفوفه.
إظهار الإجابة النموذجية
بيانات لا يملكها: الحساب المشترك qa.shared@example.com يمكن أن يستخدمه أو يغيّره أو يقفله أي اختبار أو مختبِر أو تشغيل موازٍ آخر. استبدل به تجهيزة customer تُنشئ عميلاً فريداً عبر API وتحذفه في التفكيك، وسجّل الدخول عبر API أو بجلسة محفوظة بدلاً من نموذج تسجيل الدخول، الذي يستحق اختباراً خاصاً به.
تنظيف يُتخطى وحالة تتسرب: طلب DELETE في نهاية الجسم لا يعمل أبداً عندما يفشل تأكيد، فتتراكم الطلبات. والحالات الثلاث تتشارك طلباً واحداً: بعد تطبيق SAVE10 تبدأ الحالة الثانية من طلب مخفَّض أصلاً، وإذا فشلت الحالة الأولى لا تعمل البقية إطلاقاً. ولا يُظهر تقرير التشغيل إلا اختباراً واحداً للحالات الثلاث.
منطق مخبأ: applyTwice والقيمة السحرية "error" يغيّران ما يفعله الاختبار. فـ«تطبيق الرمز مرتين يُبقي خصماً واحداً» اختبار مستقل، والخطأ المتوقع مكانه عمود خاص به يذكر الخطأ المحدد، لا فحص تنبيه عام. والعنوان coupon tests لا يقول شيئاً؛ فكل اختبار جديد يذكر اسم حالته.
البديل: تجهيزة customer (إنشاء، ثم use، ثم حذف)، وتجهيزة createOrder تطلب customer وتعطي الاختبار دالة: تُنشئ طلباً بالإجمالي المأخوذ من الصف وتسجّل معرّفه، ويحذف تفكيكها كل طلب سجّلته قبل أن تحذف تجهيزة customer العميل. ولا حاجة إلى beforeEach: فكل اختبار يفتح صفحة طلبه بعد إنشائه، والخطاف هنا سيُخفي الخطوة الوحيدة المختلفة. وللجدول الأعمدة case وcode وcart_total وexpected_total وerror، بصفوف مثل valid code وlower-case code وminimum order exactly وjust below minimum وexpired code وunknown code، وتُنشئ حلقة اختباراً بعنوان فريد لكل صف. عندها يُظهر تقرير التشغيل اختباراً لكل صف، إضافة إلى اختبار «التطبيق مرتين» المنفصل، أي سبعة اختبارات مع هذه الصفوف الستة، وكل منها يبدأ من طلب جديد خاص به.
أسئلة شائعة
هل أستخدم الخطافات أم التجهيزات؟
إذا كان إطار عملك يوفر التجهيزات، كما في Playwright Test وpytest، فاستخدمها لكل ما ينتج قيمة ويحتاج إلى تفكيك، مثل عميل أو طلب: فهي تعمل فقط للاختبارات التي تطلبها وتُبقي الإعداد والتنظيف معاً. أما الخطافات فتناسب الخطوات البسيطة التي يحتاجها كل اختبار في الملف، مثل فتح الصفحة نفسها. وفي Jest وVitest وJUnit تؤدي الخطافات مع الدوال المساعدة أو دوال المصنع المهمة نفسها.
هل يكون ملف البيانات CSV أم JSON؟
يناسب CSV الجداول المسطحة: يُفتح في برنامج جداول بيانات، ويستطيع غير المبرمج مراجعته، وتظهر تغييراته بوضوح في المقارنة (Diff)، لكن كل قيمه نصوص، والقيم التي تحوي فواصل تحتاج إلى علامات اقتباس. ويناسب JSON البيانات المتداخلة، والقيم ذات الأنواع مثل الأرقام والقيم المنطقية وnull، وأجسام طلبات API. وفي الحالتين أبقِ الملف في المستودع بجوار الاختبارات التي تستخدمه.
هل البيانات العشوائية فكرة جيدة؟
القيم العشوائية جيدة للتفرّد، مثل عناوين البريد والمعرّفات التي يجب ألا تتصادم. أما القيم التي تحدد النتيجة المتوقعة ففضّل لها بيانات ثابتة مختارة عن قصد. وإذا ولّدت مدخلات لتغطية أوسع، فسجّل القيم أو البذرة العشوائية (Seed) حتى يمكن تكرار التشغيل الفاشل بالضبط.
المراجع
- ISTQB Glossaryالتعريفات القياسية لمصطلحات الاختبار — حالة الاختبار واختبار الانحدار والخطورة والأولوية وتحليل القيم الحدّية وغيرها.
- Playwright Documentationالتوثيق الرسمي لـ Playwright، إطار الاختبار الشامل الذي يشغّل Chromium وWebKit وFirefox.
- Eradicating Non-Determinism in Tests (Martin Fowler)لماذا تُفقد الاختبارات المتذبذبة الحزمةَ قيمتها، وأسبابها الشائعة (العزل وعدم التزامن والخدمات البعيدة والوقت وتسرّب الموارد)، والحجر كاستجابة.
- RFC 4180: Common Format and MIME Type for Comma-Separated Values (CSV) Filesقواعد CSV الشائعة: الفاصلة كفاصل، ونهايات الأسطر CRLF، وتحويط الحقول التي تحتوي فواصل أو علامات اقتباس أو أسطر جديدة.
تدرّب بأدوات متقن
ولّد ملفات CSV بأعمدة وأنواع بيانات وأعداد صفوف وفواصل مخصصة.
تحويل بيانات CSV أو TSV إلى مصفوفة JSON من الكائنات.
مصنع متكامل لبيانات الاختبار الاصطناعية: مستخدمون وعملاء وشركات وعناوين وأرقام وتواريخ وسلاسل نصية بصيغة JSON أو CSV أو SQL أو XML.
ولّد سجلات مستخدمين وهمية (الاسم واسم المستخدم والبريد الإلكتروني وكلمة المرور وبذرة الصورة الرمزية والدور) لحسابات الاختبار.
توليد معرّفات UUID v4 (عشوائية) وv7 (مرتّبة زمنياً) بالجملة، والتحقق من المعرّفات الموجودة.
التحقق من صحة صياغة JSON مع تحديد مواقع الأخطاء بدقة واقتراحات مفيدة للإصلاح.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.