ازاي تسرع ال PHP Tests بتاعتك باستخدام GitHub Actions

ازاي تسرع ال PHP Tests بتاعتك باستخدام GitHub Actions

قسم ال tests بتاعتك على عدة jobs وشغلهم مع بعض عشان توفر وقت كتير
8 دقائق للقراءة
Jun 27, 2025 10:00 AM (a month ago)

ال tests الطويلة دي مرهقة بجد. كنت بستنى اكتر من 15 دقيقة عشان ال CI يخلص وده كان بيخليني اتعب من الانتظار.

فاكتشفت طريقة اقسم بيها ال tests على jobs كتيرة وأشغلهم parallel. النتيجة؟ وقت ال tests قل من 15 دقيقة لـ 4 دقائق بس.

هشرحلك ال setup اللي انا بستخدمه دلوقتي. مش معقد بس محتاج تركيز شوية.

Sequential vs Parallel Testing: قبل

Sequential vs Parallel Testing: بعد

# ده لمين؟

لو انت مطور Laravel وبتواجه نفس المشكلة - tests بطيئة في ال CI - يبقا المقال ده ليك.

# المشكلة

كل ما بضيف features ال tests بتزيد وتبقا ابطأ. اللي كان 30 ثانية بقا 15 دقيقة في ال CI.

ال solution مش انك تشتري runners اقوى. ال solution انك تقسم ال tests وتشغلهم مع بعض.


# ال Setup

انا بستخدم 3 jobs:

name: tests

on:
  push:
    branches: ['*']
  pull_request:
    branches: ['*']

بيشتغل على اي push او PR.

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

لو عملت commits كتير ورا بعض، هيلغي القديمة ويشغل الجديدة بس. بيوفر فلوس.


# Job 1: البناء (prepare)

ده بيعمل build للـ app مرة واحدة. باقي ال jobs تاخد النتيجة.

prepare:
  name: Prepare deps, assets & .env
  runs-on: ubuntu-latest
  container:
    image: lorisleiva/laravel-docker:8.3

بستخدم Docker container عشان ميحصلش مشاكل versions.

steps:
  - uses: actions/checkout@v4

نزل ال code.

- name: Disable Xdebug & OPCache for CLI
  run: |
    echo "xdebug.mode=off"      > /usr/local/etc/php/conf.d/disable-xdebug.ini
    echo "opcache.enable_cli=0" >> /usr/local/etc/php/conf.d/disable-opcache.ini

قفل Xdebug عشان مبطئ.

- name: Install Node.js & npm
  run: apk add --no-cache nodejs npm

لو بتستخدم Filament او Inertia محتاج Node. لو Laravel API بس، شيل دي.

- name: Cache Composer cache
  uses: actions/cache@v4
  with:
    path: ~/.composer/cache
    key: ${{ runner.os }}-composer-cache-${{ hashFiles('composer.lock') }}
    restore-keys: ${{ runner.os }}-composer-cache-

cache للتنزيلات.

- name: Cache vendor
  uses: actions/cache@v4
  with:
    path: vendor
    key: ${{ runner.os }}-vendor-${{ hashFiles('composer.lock') }}
    restore-keys: ${{ runner.os }}-vendor-

cache للـ vendor folder.

- name: Install PHP dependencies
  run: composer install --no-interaction --prefer-dist --no-scripts --no-progress

ثبت ال packages.

- name: Cache Node modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package.json') }}
    restore-keys: ${{ runner.os }}-node-modules-

cache للـ npm.

- name: Install & build JS assets
  run: |
    npm ci --silent
    npm run build

build للـ frontend.

- name: Copy test .env
  run: cp .env.example .env

- name: Generate application key
  run: php artisan key:generate --ansi

حضر Laravel.

- name: Upload build & .env
  uses: actions/upload-artifact@v4
  with:
    name: build-env
    path: |
      public/build
      .env

ارفع النتيجة. ال test jobs هتنزلها.


# Job 2: ال Tests (tests-be)

هنا السحر. نقسم ال tests ونشغل كل جزء منفصل.

tests-be:
  name: Run unit tests (BE)   shard ${{ matrix.ci_node_index }}/6
  needs: prepare
  runs-on: ubuntu-latest
  container:
    image: lorisleiva/laravel-docker:8.3

6 jobs في نفس الوقت.

strategy:
  fail-fast: true
  matrix:
    ci_node_index: [1, 2, 3, 4, 5, 6]
    ci_node_total: [6]

كل job بياخد index مختلف. بيقسم ال tests بناء عليه.

steps:
  - uses: actions/checkout@v4

كل job بيبدأ من الصفر.

- name: Install GNU coreutils (for split -d)
  run: apk add --no-cache coreutils

محتاج split عشان اقسم ال tests.

- name: Disable Xdebug & OPCache for CLI
  run: |
    echo "xdebug.mode=off" > /usr/local/etc/php/conf.d/disable-xdebug.ini
    echo "opcache.enable_cli=0" >> /usr/local/etc/php/conf.d/disable-opcache.ini

- name: Increase PHP memory limit
  run: echo "memory_limit=-1" > /usr/local/etc/php/conf.d/memory.ini

قفل Xdebug وزود الـ memory.

- name: Cache Composer cache
  uses: actions/cache@v4
  with:
    path: ~/.composer/cache
    key: ${{ runner.os }}-composer-cache-${{ hashFiles('composer.lock') }}
    restore-keys: ${{ runner.os }}-composer-cache-

- name: Cache vendor
  uses: actions/cache@v4
  with:
    path: vendor
    key: ${{ runner.os }}-vendor-${{ hashFiles('composer.lock') }}
    restore-keys: ${{ runner.os }}-vendor-

- name: Install PHP dependencies (if not cached)
  run: composer install --no-interaction --prefer-dist --no-scripts --no-progress

نرجع ال PHP dependencies من ال cache.

- name: Download build & .env
  uses: actions/download-artifact@v4
  with:
    name: build-env
    path: .

نزل الـ build وال .env.

- name: Copy .env
  run: cp .env.example .env

- name: Generate application key
  run: php artisan key:generate --ansi

حضر Laravel.

- name: Ensure Paratest cache dir exists
  run: mkdir -p storage/framework/cache

Paratest محتاج المجلد ده.

- id: split
  uses: remarkablemark/find-and-split@v1
  with:
    directory: tests
    pattern: '*Test.php'
    chunk: ${{ matrix.ci_node_index }}/${{ matrix.ci_node_total }}

بيقسم ال tests لـ 6 مجموعات.

- name: Run Pest (shard ${{ matrix.ci_node_index }})
  run: |
    FILES="${{ steps.split.outputs.files }}"
    PATTERNS=$(for f in $FILES; do basename "$f" .php; done | paste -sd '|' -)
    php -d memory_limit=-1 artisan test --filter "/($PATTERNS)/" --parallel \
      --cache-directory=storage/framework/cache --stop-on-failure

شغل ال tests بتاعت ال shard ده parallel.


# Job 3: خلاص (finish-tests)

finish-tests:
  name: Finish tests
  needs: tests-be
  runs-on: ubuntu-latest
  steps:
    - run: echo "All shards passed!"

بيشتغل بس لو كل حاجة نجحت. ممكن تستخدمه لـ deployment او اشعارات.


# خلاصة

Setup بسيط وبيشتغل مع Pest و PHPUnit. الوقت بتاعي قل من 15 دقيقة لـ 4 دقائق.

لو عندك tests كتيرة اوي، ممكن تزود العدد من 6 لـ 8 او 10. او تضيف browser tests للـ matrix.

جربه وهتشوف الفرق بنفسك.


# ليه مقسمناش كله في job واحد؟

لو حطيت كل حاجة في job واحد، كل واحد من الـ 6 jobs هيعمل:

  • يثبت Composer dependencies
  • يثبت npm dependencies
  • يبني frontend assets
  • يحضر Laravel environment
  • يشغل ال tests

ده شغل مكرر كتير.

بدل كده، بنعمل الشغل ده مرة واحدة في job منفصل وكل ال test jobs تاخد النتيجة الجاهزة. النتيجة؟ وقت اقل بكتير.


# الكود كامل

هنا الـ .github/workflows/tests.yml كامل:

name: tests

on:
  push:
    branches: ['*']
  pull_request:
    branches: ['*']

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # ────────────────────────────────────────────────────────────────
  # 1) Prepare: install, build, and bundle .env for all shards
  # ────────────────────────────────────────────────────────────────
  prepare:
    name: Prepare deps, assets & .env
    runs-on: ubuntu-latest
    container:
      image: lorisleiva/laravel-docker:8.3

    steps:
      - uses: actions/checkout@v4

      - name: Disable Xdebug & OPCache for CLI
        run: |
          echo "xdebug.mode=off"      > /usr/local/etc/php/conf.d/disable-xdebug.ini
          echo "opcache.enable_cli=0" >> /usr/local/etc/php/conf.d/disable-opcache.ini

      # ── Add Node.js (for npm build) ───────────────────────────────
      - name: Install Node.js & npm
        run: apk add --no-cache nodejs npm

      # ── Composer cache & install ─────────────────────────────────
      - name: Cache Composer cache
        uses: actions/cache@v4
        with:
          path: ~/.composer/cache
          key: ${{ runner.os }}-composer-cache-${{ hashFiles('composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-cache-

      - name: Cache vendor
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-vendor-${{ hashFiles('composer.lock') }}
          restore-keys: ${{ runner.os }}-vendor-

      - name: Install PHP dependencies
        run: composer install --no-interaction --prefer-dist --no-scripts --no-progress

      # ── JS caches & build ─────────────────────────────────────────
      - name: Cache Node modules
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package.json') }}
          restore-keys: ${{ runner.os }}-node-modules-

      - name: Install & build JS assets
        run: |
          npm ci --silent
          npm run build

      # ── Bundle .env & assets ─────────────────────────────────────
      - name: Copy test .env
        run: cp .env.example .env

      - name: Generate application key
        run: php artisan key:generate --ansi

      - name: Upload build & .env
        uses: actions/upload-artifact@v4
        with:
          name: build-env
          path: |
            public/build
            .env

  # ────────────────────────────────────────────────────────────────
  # 2) Test Shards: download artifact, restore cache, split & run
  # ────────────────────────────────────────────────────────────────
  tests-be:
    name: Run unit tests (BE)   shard ${{ matrix.ci_node_index }}/6
    needs: prepare
    runs-on: ubuntu-latest
    container:
      image: lorisleiva/laravel-docker:8.3

    strategy:
      fail-fast: true
      matrix:
        ci_node_index: [1, 2, 3, 4, 5, 6]
        ci_node_total: [6]

    steps:
      - uses: actions/checkout@v4

      - name: Install GNU coreutils (for split -d)
        run: apk add --no-cache coreutils

      - name: Disable Xdebug & OPCache for CLI
        run: |
          echo "xdebug.mode=off"      > /usr/local/etc/php/conf.d/disable-xdebug.ini
          echo "opcache.enable_cli=0" >> /usr/local/etc/php/conf.d/disable-opcache.ini

      - name: Increase PHP memory limit
        run: echo "memory_limit=-1" > /usr/local/etc/php/conf.d/memory.ini

      # ── Restore Composer cache & vendor ──────────────────────────
      - name: Cache Composer cache
        uses: actions/cache@v4
        with:
          path: ~/.composer/cache
          key: ${{ runner.os }}-composer-cache-${{ hashFiles('composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-cache-

      - name: Cache vendor
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-vendor-${{ hashFiles('composer.lock') }}
          restore-keys: ${{ runner.os }}-vendor-

      - name: Install PHP dependencies (if not cached)
        run: composer install --no-interaction --prefer-dist --no-scripts --no-progress

      # ── Download build assets & .env ─────────────────────────────
      - name: Download build & .env
        uses: actions/download-artifact@v4
        with:
          name: build-env
          path: .

      - name: Copy .env
        run: cp .env.example .env

      - name: Generate application key
        run: php artisan key:generate --ansi

      - name: Ensure Paratest cache dir exists
        run: mkdir -p storage/framework/cache

      # ── Split tests & run Pest ───────────────────────────────────
      - id: split
        uses: remarkablemark/find-and-split@v1
        with:
          directory: tests
          pattern: '*Test.php'
          chunk: ${{ matrix.ci_node_index }}/${{ matrix.ci_node_total }}

      - name: Run Pest (shard ${{ matrix.ci_node_index }})
        run: |
          FILES="${{ steps.split.outputs.files }}"
          PATTERNS=$(for f in $FILES; do basename "$f" .php; done | paste -sd '|' -)
          php -d memory_limit=-1 artisan test --filter "/($PATTERNS)/" --parallel \
            --cache-directory=storage/framework/cache --stop-on-failure

  finish-tests:
    name: Finish tests
    needs: tests-be
    runs-on: ubuntu-latest
    steps:
      - run: echo "All shards passed!"

# استنى الجزء التاني قريب!

شغل ال parallel jobs ده بيسرع ال tests بشكل جامد، بس ايه اللي يحصل لما تعدي الـ 2,000 دقيقة المجانية بتاعت GitHub في الشهر؟ ممكن تلاقي نفسك بتدفع فلوس كتير مع ال GitHub-hosted runners العادية.

في الجزء التاني من السلسلة دي، هقولك ازاي تشغل GitHub Actions بـ 4 دولار بس في الشهر باستخدام حلول رخيصة حتى لو انت بتشغل الاف دقائق tests.

هغطي ال self-hosted runners على cloud providers رخيصين، ال spot instances وازاي توفر فلوس، امتى يكون من المنطق تسيب ال GitHub-hosted runners، ومقارنات تكلفة حقيقية مع تحليل break-even.

مناسب للفرق اللي بتكبر ومحتاجة CI/CD اكتر من غير ما تكسر الميزانية. استنونا!

القائمة البريدية

كل فترة ببعت ايميلات عن المواضيع الجديدة اللي بنزلها او بعلن عن مشاريع جديدة شغال عليها اذا حبب تكون اول حد يعرف اشترك من هنا.

مفيش سبام او ايميلات في الفاضي 💎

التعليقات

Ahmed Nagi - Powerd By Vuepress . Hosted with GitHub and Netlify .