cloud-devops

حدود تغطية Vitest: إيقاف الـ CI عند انخفاض التغطية (2026)

٢٣ يونيو ٢٠٢٦

Vitest Coverage Thresholds: Fail CI on Low Coverage (2026)

لجعل Vitest يفشل في CI عند انخفاض التغطية (coverage)، قم بتعيين حدود رقمية (thresholds) تحت coverage.thresholds في vitest.config.ts وقم بتشغيل vitest run --coverage. إذا انخفض أي مقياس عن حده المعين، سيقوم Vitest بطباعة خطأ والخروج بكود 1، مما يؤدي لفشل مهمة CI.1

ملخص

في Vitest 4، توجد حدود التغطية تحت coverage.thresholds (وليس كمفاتيح مسطحة مثل coverage.lines). قم بتعيين lines، و functions، و branches، و statements، ثم قم بتشغيل vitest run --coverage؛ أي نقص سيؤدي للخروج بكود غير صفري وكسر عملية البناء (build). السبب الأكثر شيوعاً لعدم "فشل" الحد هو الإعداد المسطح (flat config) — أي وضع الأرقام مباشرة تحت coverage بدلاً من coverage.thresholds — وهو ما يتجاهله Vitest بصمت. يوفر لك هذا الدليل إعدادات Vitest 4 قابلة للتشغيل، وبوابات لكل ملف (per-file) ولأنماط glob، وسير عمل GitHub Actions — وكلها تم التحقق منها مقابل vitest@4.1.9.2

ما ستتعلمه

  • كيفية جعل Vitest يفشل في CI عندما تنخفض التغطية عن حد معين
  • لماذا لا يفشل حد التغطية الخاص بك في كسر البناء (فخ التجاهل الصامت)
  • ما الذي تغير في التغطية في Vitest 4
  • كيفية تعيين حدود لكل ملف ولأنماط glob
  • ماذا تفعل thresholds.100 و autoUpdate
  • سواء كنت ستستخدم مزود التغطية v8 أو istanbul
  • كيفية فرض تغطية Vitest في GitHub Actions، بما في ذلك تعليقات PR

كيف أجعل Vitest يفشل في CI عند انخفاض التغطية؟

أضف حدوداً رقمية تحت coverage.thresholds وقم بالتشغيل باستخدام --coverage. عندما يقل أي مقياس عن حده، يخرج Vitest بكود 1 ويفشل CI.1

قم بتثبيت مشغل الاختبارات ومزود التغطية:

npm i -D vitest @vitest/coverage-v8

أنشئ ملف مصدر صغيراً للاختبار:

// src/math.ts
export function add(a: number, b: number): number {
  return a + b
}

export function isPositive(n: number): boolean {
  if (n > 0) {
    return true
}
  return false
}

قم بتهيئة الحدود. في Vitest 4 توضع تحت coverage.thresholds:

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json-summary', 'json'],
      include: ['src/**/*.ts'],
      thresholds: {
        lines: 90,
        functions: 90,
        branches: 90,
        statements: 90,
      },
    },
  },
})

أضف اختباراً يغطي جزءاً فقط من الملف:

// test/math.test.ts
import { expect, test } from 'vitest'
import { add } from '../src/math'

test('add', () => {
  expect(add(2, 3)).toBe(5)
})

قم بتشغيله:

npx vitest run --coverage

لأن isPositive غير مختبر، سيقوم Vitest بطباعة النقص والخروج بكود غير صفري:

ERROR: Coverage for lines (25%) does not meet global threshold (90%)
ERROR: Coverage for functions (50%) does not meet global threshold (90%)
ERROR: Coverage for statements (25%) does not meet global threshold (90%)
ERROR: Coverage for branches (0%) does not meet global threshold (90%)

تخرج العملية بكود 1. أضف اختباراً يمر على isPositive (كلا الفرعين) وستصل التغطية إلى 100%، لذا سيخرج نفس الأمر بكود 0. هذا الخروج غير الصفري هو الآلية بالكامل: أي مشغل CI سيعتبره خطوة فاشلة. لا تحتاج إلى إجراء "بوابة تغطية" منفصل للفشل الصريح — كود الخروج الخاص بـ Vitest هو البوابة.

الحد الموجب هو الحد الأدنى للنسبة المئوية. الحد السالب هو الحد الأقصى لعدد العناصر غير المغطاة المسموح بها: lines: -10 تعني "لا يسمح بأكثر من 10 أسطر غير مغطاة".1

لماذا لا يفشل حد تغطية Vitest الخاص بي في كسر البناء؟

السبب المعتاد هو الإعداد المسطح. يجب أن تكون الحدود متداخلة تحت coverage.thresholds — وهو التنسيق الذي يتطلبه Vitest منذ الإصدار 1.0 — لذا فإن الإعداد المسطح coverage: { lines: 90 } يتم تجاهله بصمت، ويخرج التشغيل بكود 0، ولا يتم فرض أي بوابة.1

هذا الإعداد — المنسوخ من عدد لا يحصى من البرامج التعليمية القديمة — لا يفرض أي شيء في إصدار Vitest الحالي:

// vitest.config.ts — BROKEN: thresholds must be nested (silently does nothing)
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
      // ❌ flat keys are ignored — they must live under `thresholds`
      lines: 90,
      functions: 90,
      branches: 90,
      statements: 90,
    },
  },
})

قم بتشغيله مقابل نفس الملف المختبر جزئياً وسيخرج Vitest بكود 0 دون أي أسطر ERROR على الإطلاق. الحل هو تغليف الأرقام في thresholds، تماماً كما في الإعداد الصحيح أعلاه. إذا كانت "بوابتك" تظهر باللون الأخضر لشهور، فتأكد من أنها تطبع بالفعل أسطر ERROR: في فرع غير مختبر عمداً قبل أن تثق بها.

سبب ثانٍ وأكثر دقة هو إزالة coverage.all في Vitest 4. سابقاً، كان Vitest يدرج كل ملف مطابق في التقرير افتراضياً؛ أما الآن فهو يدرج فقط الملفات التي تم تحميلها أثناء تشغيل الاختبار ما لم تقم بتعيين coverage.include.3 بدون include، فإن ملف المصدر الذي لا يحتوي على أي استيرادات (imports) في اختباراتك لن يظهر أبداً في التقرير، لذا ستبدو نسبتك العالمية جيدة بينما تظل وحدات كاملة غير مقاسة. قم دائماً بتعيين coverage.include لأنماط globs الخاصة بمصدرك حتى يتم احتساب الملفات غير المختبرة ضدك.

ما الذي تغير في التغطية في Vitest 4؟

كائن coverage.thresholds نفسه لم يتغير — لقد كان التنسيق المطلوب منذ Vitest 1.0. ما غيره Vitest 4 هو تقرير التغطية: حيث قام بإزالة coverage.all، وإزالة ignoreEmptyLines، وجعل إعادة تعيين V8 القائمة على AST هي الوضع الوحيد. يتطلب Vitest 4.0 أيضاً Vite ≥ 6.0.0 و Node.js ≥ 20.0.0.3

المجالVitest 3Vitest 4
نطاق التقريرcoverage.all كان افتراضياً true (كل ملف مطابق)تمت إزالة coverage.all؛ يغطي التقرير فقط الملفات المحملة ما لم يتم تعيين coverage.include3
الأسطر الفارغةignoreEmptyLines كان متاحاًتمت إزالته؛ الأسطر التي لا تحتوي على كود برمجي لم تعد تُحتسب3
إعادة تعيين V8v8-to-istanbul افتراضياً (إعادة تعيين AST اختيارية منذ v3.2.0)إعادة التعيين القائمة على AST هي الوضع الافتراضي والوحيد3

لأن هيكل الحدود لم يتغير، فإن الإعداد المكتوب لـ Vitest 1–3 يستمر في العمل تحت Vitest 4. النقطة المهمة هي الدقة: إعادة تعيين V8 الأكثر دقة يمكن أن تغير النسب المئوية المبلغ عنها قليلاً، لذا أعد تحديد أرقامك الأساسية بعد الترقية.3

كيف يمكنني تعيين حدود تغطية لكل ملف ولأنماط glob في Vitest؟

قم بتعيين thresholds.perFile: true لتطبيق الأرقام العالمية على كل ملف على حدة، وأضف مفاتيح أنماط glob لفرض أرقام أكثر صرامة على مسارات محددة.1

عند انتهاك إدخال glob، يحدد Vitest كلاً من القاعدة والملف، على سبيل المثال: Coverage for lines (25%) does not meet "src/**/math.ts" threshold (100%) for src/math.ts. ملاحظة هامة من التوثيق: يحسب Vitest الملفات التي تطابق أنماط glob ضمن العتبات العامة (global) أيضاً — على عكس Jest، الذي يخرجها من النطاق.1 أيضاً، يحدد إدخال glob فقط المقاييس التي تدرجها؛ ولا يرث الأرقام العامة. إذا حددت كتلة glob فقط lines، فسيتم فرض lines فقط لتلك الملفات.1

ماذا تفعل thresholds.100 و autoUpdate؟

thresholds.100: true هو اختصار يضبط كلاً من lines و functions و branches و statements جميعاً على 100. أما thresholds.autoUpdate: true فيقوم برفع عتباتك تلقائياً في ملف الإعداد كلما تجاوزت التغطية الحالية هذه العتبات.1

// 100% في كل مكان، بالطريقة المختصرة
export default defineConfig({
  test: {
    coverage: {
      include: ['src/**/*.ts'],
      thresholds: { 100: true },
    },
  },
})

يمكنك أيضاً تمرير --coverage.thresholds.100 عبر واجهة سطر الأوامر (CLI) لتشغيل صارم لمرة واحدة. autoUpdate هو "ترس التغطية": عندما ترفع عملية Pull Request التغطية من 85% إلى 88%، يقوم Vitest بإعادة كتابة 85 لتصبح 88 في إعداداتك حتى لا ينخفض المستوى أبداً. مرر دالة للتحكم في التنسيق، مثل autoUpdate: (newThreshold) => Math.floor(newThreshold) للحفاظ على الأرقام الصحيحة.1 استخدم autoUpdate محلياً أو في مهمة مخصصة — لا ترغب في إعادة كتابة الإعدادات أثناء تشغيل CI عادي.

هل يجب أن أستخدم مزود التغطية v8 أم istanbul؟

استخدم v8 (الافتراضي) لمعظم مشاريع Node والمتصفح؛ انتقل إلى istanbul فقط عندما تعمل في بيئة لا تتوفر فيها تغطية V8، مثل Firefox أو Bun أو Cloudflare Workers.4 يستخدم V8 إعادة تعيين (remapping) تعتمد على AST — تم تقديمها في v3.2.0 وهي الوضع الافتراضي والوحيد منذ Vitest 4 — لذا أصبحت تقاريره الآن بدقة تقارير Istanbul، واختفت إلى حد كبير المقايضة التاريخية "V8 سريع ولكنه غير دقيق".4

# v8 (الافتراضي) — سريع، لا يحتاج لتجهيز مسبق (pre-instrumentation)
npm i -D @vitest/coverage-v8

# istanbul — توافق شامل مع بيئات التشغيل
npm i -D @vitest/coverage-istanbul

يدعم كلا المزودين نفس خيارات thresholds، لذا يمكنك التبديل بين المزودين دون إعادة كتابة القواعد الخاصة بك.1 التقارير الافتراضية هي ['text', 'html', 'clover', 'json']؛ أضف json-summary إذا كنت تخطط لنشر التغطية في طلبات السحب (القسم التالي).5

كيف يمكنني فرض تغطية Vitest في GitHub Actions؟

قم بتشغيل vitest run --coverage في سير العمل (workflow). تجعل العتبات الخطوة تفشل عند انخفاض التغطية؛ أضف تقرير json-summary وإجراء تقارير (reporting action) إذا كنت تريد أيضاً تعليقاً على PR.56

# .GitHub/workflows/test.yml
name: test
on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  pull-requests: write   # مطلوب فقط لخطوة التعليق على PR

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      # تفشل المهمة إذا لم يتم استيفاء أي عتبة (يخرج Vitest بكود غير صفري)
      - run: npx vitest run --coverage
      # اختياري: نشر / تحديث تعليق التغطية على PR
      - if: GitHub.event_name == 'pull_request'
        uses: davelosert/vitest-coverage-report-action@v2

هناك أمران يجب التمييز بينهما. أولاً، العتبة الصارمة هي خروج Vitest نفسه بكود غير صفري بناءً على العتبات التي قمت بإعدادها — يمكن لـ branch protection حينها طلب فحص coverage قبل الدمج. ثانياً، يقوم davelosert/vitest-coverage-report-action بالإبلاغ عن التغطية ومقارنتها في تعليق PR واحد يتم تحديثه تلقائياً؛ يتطلب ذلك تقرير json-summaryjson للتفاصيل لكل ملف) في إعدادات Vitest وإذن pull-requests: write.6 احتفظ بكل من خطوة العتبة وخطوة التعليق: إحداهما تمنع الدمج، والأخرى توضح للمراجعين ما الذي تغير.

الخلاصة والخطوات التالية

عتبات التغطية تحميك فقط إذا كانت تؤدي بالفعل لفشل عملية البناء. في Vitest 4، يعني هذا وضع الأرقام داخل coverage.thresholds، وضبط coverage.include، والتأكد من أن الفرع (branch) الذي تنقصه الاختبارات عمداً يطبع أسطر ERROR: ويخرج بكود غير صفري. قم بإضافة قواعد لكل ملف أو عبر glob للوحدات البرمجية الحرجة، وأضف autoUpdate للرفع التلقائي، واربط vitest run --coverage في CI مع حماية الفروع (branch protection).

من هنا، يمكنك تعميق مسار الاختبار الخاص بك: اطلع على دليلنا حول استراتيجيات اختبار الوحدات (unit testing) لكود موثوق، والشرح التفصيلي حول اختبار تطبيقات AWS CDK في TypeScript للتأكد من البنية التحتية، و اختبار مطالبات LLM في CI باستخدام Promptfoo عندما تغطي اختباراتك سلوك الذكاء الاصطناعي.

Footnotes

  1. Vitest — coverage config (coverage.thresholds, perFile, autoUpdate, 100, glob patterns). https://vitest.dev/config/coverage 2 3 4 5 6 7 8 9 10 11 12 13 14

  2. npm registry, verified 2026-06-23: vitest@4.1.9, @vitest/coverage-v8@4.1.9. Reproduced on Node v22.22.3. https://www.npmjs.com/package/vitest

  3. Vitest — Migrating to Vitest 4.0 (prerequisites Vite ≥ 6 / Node ≥ 20; removal of coverage.all, coverage.extensions, coverage.ignoreEmptyLines; AST-based V8 remapping). https://vitest.dev/guide/migration#vitest-4 2 3 4 5 6 7

  4. Vitest — Coverage guide (v8 vs istanbul providers and runtime compatibility). https://vitest.dev/guide/coverage 2 3

  5. Vitest — coverage.reporter (default ['text','html','clover','json']). https://vitest.dev/config/coverage#coverage-reporter 2

  6. davelosert/vitest-coverage-report-action — requires the json-summary reporter and pull-requests: write; posts an auto-updating PR comment. https://GitHub.com/davelosert/vitest-coverage-report-action 2 3

الأسئلة الشائعة

قم بضبط عتبات رقمية تحت coverage.thresholds في vitest.config.ts وقم بتشغيل vitest run --coverage . أي نقص سيؤدي للخروج بكود 1 ، مما يفشل خطوة CI. 1