انتقل إلى المحتوى
Dazvix
المدوّنة
ضمان الجودة4 دقيقة قراءة

إعداد ضمان الجودة الذي يلتقط الأخطاء قبل أن يلتقطها مستخدموك

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

هناك طريقتان تموت بهما حزمة الاختبارات. إما أن تكون هزيلة، فتصل الأخطاء إلى بيئة الإنتاج ويكفّ الفريق عن الإيمان بجدوى الاختبار. أو أن تكون ضخمة وبطيئة، فيستغرق المسار أربعين دقيقة، ويدمج الناس الشيفرة والاختبارات حمراء، ويكفّ الفريق عن الإيمان بجدوى الاختبار. والنهاية واحدة في الحالتين.

ما يلي هو الشكل الذي نعتمده افتراضيًا. ليست حزمة الاختبارات الأكبر، بل الحزمة التي تستمر في سداد كلفتها بعد انتهاء زحمة الإطلاق.

وزّع الطبقات بحسب ما يتعطل فعلًا

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

  • اختبارات الوحدة للمنطق الحقيقي: التسعير، والصلاحيات، وحسابات التواريخ، والمحلّلات، وكل ما فيه تفرعات تستحق الحصر. سريعة ووفيرة ورخيصة الصيانة.
  • اختبارات التكامل هي مركز الثقل. اعرض المكوّن مع مكوّناته الفرعية الحقيقية، وتفاعل معه كما يفعل المستخدم، وتحقق مما يظهر. استخدم Testing Library بدلًا من العرض السطحي المثقل بالمحاكاة.
  • اختبارات E2E لحفنة الرحلات التي لا يحتمل العمل تعطّلها. من خمس إلى خمس عشرة، لا مئتان.
  • اختبارات العقود أينما كانت الحدود مملوكة لطرف آخر.

اكتب اختبارات E2E كما يصفها المستخدم

السبب الأكبر وراء حزم E2E المتقلبة وعالية الصيانة هو محددات (selectors) مرتبطة ببنية الشيفرة. فصنف CSS تفصيل في التنفيذ، أما الدور والاسم المُتاح للوصول فهما العقد الذي يعيشه المستخدم فعلًا، والتحقق منهما يجعل الاختبار فحصًا لإمكانية الوصول أيضًا.

import { test, expect } from '@playwright/test';

test('a visitor can send a project inquiry', async ({ page }) => {
  await page.goto('/contact');

  // Role + accessible name: stable across restyles, and it fails
  // loudly if the field ever loses its label.
  await page.getByLabel('Name').fill('Jordan Ellis');
  await page.getByLabel('Email').fill('jordan@example.com');
  await page.getByLabel('Message').fill('We need a WooCommerce migration.');
  await page.getByRole('button', { name: 'Send message' }).click();

  // Assert the user-visible outcome, never an internal state flag.
  await expect(page.getByRole('status')).toContainText('Thanks');
});

قاعدتان تحافظان على صحة هذه الحزم. لا تستخدم انتظارًا ثابتًا أبدًا، فـ waitForTimeout هو الطريق إلى حزمة بطيئة ومتقلبة في آن واحد؛ وPlaywright ينتظر تلقائيًا في تحققاته، فدعه يفعل. وتحقق مما يراه المستخدم، لا من اسم صنف أو قيمة في المخزن، لأن هذه تتغير لأسباب لا علاقة لها بما إذا كانت الميزة تعمل.

اختبارات العقود تلتقط ما لا يلتقطه غيرها

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

يتحقق اختبار العقد من الاستجابة الحقيقية مقابل مخطط، وهو أرخص تأمين في الحزمة.

import { z } from 'zod';

const Contact = z.object({
  id: z.string(),
  properties: z.object({
    email:     z.string().email(),
    lifecyclestage: z.enum(['subscriber', 'lead', 'customer']),
    createdate: z.string().datetime(),
  }),
});

test('HubSpot contact shape has not drifted', async () => {
  const res = await fetch(`${API}/crm/v3/objects/contacts/${FIXTURE_ID}`, {
    headers: { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}` },
  });

  // Fails the moment the provider adds a status or tightens a field.
  expect(() => Contact.parse(res.json())).not.toThrow();
});

شغّل هذه الاختبارات وفق جدول زمني، لا في CI فقط. فالعقد لا ينكسر حين تنشر أنت، بل حين ينشر الطرف الآخر، وقد يكون ذلك في أي يوم ثلاثاء.

اختبار الحمل يجيب عن سؤال لا تجيب عنه بيئة الاختبار

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

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 100 },  // ramp
    { duration: '5m', target: 100 },  // hold — this is where leaks appear
    { duration: '2m', target: 0 },
  ],
  thresholds: {
    // Fail the run, do not just draw a graph.
    http_req_duration: ['p(95)<500'],
    http_req_failed:   ['rate<0.01'],
  },
};

export default function () {
  const res = http.get(`${__ENV.BASE_URL}/work`);
  check(res, { 'status 200': (r) => r.status === 200 });
}

مرحلة الثبات لخمس دقائق أهم من الذروة. فالصعود إلى رقم كبير ثم التوقف يريك المسار السعيد فقط، أما الثبات عند الحمل فهو ما يكشف تسرّب الاتصالات، والذاكرات المؤقتة غير المحدودة، والاستعلام الذي يبدو سليمًا إلى أن تمتلئ ذاكرة المخزن المؤقت (buffer pool).

اجعل المسار سريعًا بما يكفي ليستحق الثقة

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

  1. 01قسّمه إلى مراحل. فحص الشيفرة والأنواع واختبارات الوحدة عند كل دفع، في أقل من دقيقتين. والتكامل وE2E عند طلبات الدمج. والحمل والتوافق الكامل مع المتصفحات ليلًا.
  2. 02جزّئ الطبقة البطيئة. يتجزأ Playwright بسلاسة عبر عدة مشغّلات؛ أربع شرائح تحوّل اثنتي عشرة دقيقة إلى ثلاث.
  3. 03اعزل الاختبارات المتقلبة ولا تعِد تشغيلها عشوائيًا. إعادة المحاولة الشاملة تُخفي حالة تسابق حقيقية إلى أن تحدث في الإنتاج. انقل الاختبار المتقلب إلى مهمة عزل، وأصلحه خلال أسبوع أو احذفه.
  4. 04افشل مبكرًا واعرض الفشل بوضوح. أثر التنفيذ (trace) ولقطة شاشة وفيديو عند الفشل تحوّل استنساخًا يستغرق عشرين دقيقة إلى نظرة من عشر ثوانٍ.

أين يقع الأمان

تمريرة قصيرة آلية تلتقط المشكلات الاعتيادية قبل وقت طويل من أن يصبح حجز اختبار اختراق مجديًا: تدقيق للاعتمادات عند كل بناء، وفحص للأسرار عند كل إيداع، وفحص ZAP أساسي على نسخة معاينة منشورة، وتحقق من أن ترويسات الأمان التي ضبطتها موجودة فعلًا في الاستجابة. وهذا الأخير يفشل أكثر مما يتوقع الناس، وغالبًا بعد تغيير في البنية التحتية لم يربطه أحد بالواجهة الأمامية.

الهدف ليس أقصى تغطية، بل أصغر حزمة كانت ستلتقط آخر خمسة أشياء وصلت إلى الإنتاج.