تهيئة اختبارات PowerShell عبر Pester ── الأسلوب العمليّ لجعل سكربتات التشغيل أقلّ عرضةً للكسر

· آخر تحديث: · · PowerShell, Pester, Windows, الاختبار, الأتمتة, CI, الاستفادة من الأصول القائمة

1. ما ينبغي إدراكه أوّلاً

سكربتات PowerShell تبدأ في البداية كأتمتة لعمل صغير.

جمع الملفّات. البحث في السجلّات. إنشاء CSV. نقل الملفّات القديمة. التحقّق من حالة خدمة.

كلّ هذه، طالما بقيت في عشرات الأسطر، يمكن التحقّق من عملها بالعين. لكن مع الاستمرار في استخدامها فعليّاً، تدخل تعديلات كهذه:

  • زيادة المجلّدات المستهدَفة
  • إضافة شروط استبعاد
  • تغيير أعمدة CSV
  • الأرشفة قبل الحذف
  • التشغيل من مُجدوِل المهامّ (Task Scheduler) أو من CI
  • الإشعار عند حدوث خطأ

عند هذه المرحلة، لم يعد كافياً أن «تعمل مرّة واحدة على جهازك».

خطورة PowerShell هي وجهٌ آخر لسهولته. فالقراءة وحدها يمكن تجربتها بلا قلق، لكنّ عمليّات مثل الحذف والنقل والاستبدال (overwrite) وإعادة تشغيل خدمة وتغيير الصلاحيّات، يكفي فيها خطأ بسيط في شرط ليؤدّي إلى حادث.

هنا يأتي دور Pester، إطار عمل الاختبار الخاصّ بـ PowerShell. لن نغطّي في هذا المقال جميع ميزات Pester، بل سنُنظِّم كيفيّة المضيّ في «تهيئة الاختبارات» لسكربت PowerShell قائم فعلاً، بهدف جعله أقلّ عرضةً للكسر في العمل الفعليّ.

اختبار PowerShell ليس فقط لكتابة شيفرة نظيفة. إنّه أداة لتقليل القلق قبل التعديل، والتحقّق بدليل بعد التعديل.

الشيفرات الواردة في هذا المقال متاحة كطقم أمثلة كامل (السكربت المُختبَر، اختبارات Pester، سكربت التنفيذ الخاصّ بـ CI) يمكن تشغيله عبر Invoke-Pester، وقد نشرناه على GitHub.

pester-powershell-test-maintenance - komurasoft-blog-samples (GitHub)

2. ما الذي يحميه Pester

إدخال Pester لا يجعل كلّ شيء آمناً تلقائيّاً. أوّل ما ينبغي تحديده هو «ما الذي نحميه بالاختبار».

في سكربتات تشغيل PowerShell، إعطاء الأولويّة لهذه الأمور الأربعة يُظهر أثراً سريعاً.

ما نريد حمايته ما يُنظَر إليه في الاختبار
فحص الشروط أيّ ملفّ، أيّ سطر، أيّ مستخدم، أيّ خدمة يُستهدَف
شكل المُخرَجات أسماء الأعمدة في CSV، خصائص القيمة المُعادة، عدد العناصر
ما قبل العمليّة الخطِرة هل هدف الحذف/النقل/الإيقاف مطابق للمقصود
الاعتماديّات الخارجيّة التعامل مع نظام الملفّات، وواجهات API، وتنفيذ الأوامر، والتاريخ والوقت، ومتغيّرات البيئة

وما نريد اختباره أوّلاً بشكل خاصّ ليس عمليّة الحذف نفسها، بل العمليّة التي تختار أهداف الحذف.

فمثلاً، في سكربت يحذف سجلّات قديمة، لا نختبر Remove-Item مباشرةً، بل نختبر أوّلاً «أيّ السجلّات يُختار كهدف».

هذا التقسيم يجعل الاختبار أسهل.

دالّة تجمع الأهداف
  ↓
عمليّة تتحقّق من الأهداف وتسجّلها
  ↓
عمليّة التغيير: النقل، الحذف، الإشعار، وما شابه

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

3. توحيد الإصدار

يفترض هذا المقال استخدام Pester v5.

في بيئات Windows القديمة، قد يكون Pester مُثبَّتاً مسبقاً لكن من سلسلة v3. لا تستخدم ما هو مُثبَّت في البيئة القائمة كما هو، بل تحقّق من الإصدار أوّلاً.

Get-Module Pester -ListAvailable |
  Sort-Object Version -Descending |
  Select-Object Name, Version, Path

عند التثبيت من جديد، ثبِّته من PowerShell Gallery.

Install-Module -Name Pester -Scope CurrentUser -Force -SkipPublisherCheck
Import-Module Pester
Get-Module Pester

عند الاستخدام ضمن فريق، تحقّق من عدم اختلاف إصدار Pester بين جهاز التطوير المحلّيّ، وخادم البناء (build server)، وبيئة تنفيذ المهامّ.

الارتباك الشائع في اختبارات PowerShell لا ينشأ عن مشكلة في الشيفرة، بل عن اختلاف إصدار مُشغِّل الاختبار (test runner).

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

4. تحديد مكان الملفّات

في Pester، من الشائع تسمية ملفّات الاختبار بصيغة *.Tests.ps1.

أبسط تنظيم يكون هكذا:

scripts/
  Get-OldLogFile.ps1
  Get-OldLogFile.Tests.ps1

عند اتّساع الحجم قليلاً، يُفصَل src عن tests.

src/
  public/
    Get-OldLogFile.ps1
    Remove-OldLogFile.ps1

tests/
  public/
    Get-OldLogFile.Tests.ps1
    Remove-OldLogFile.Tests.ps1

كلاهما مقبول. المهمّ هو تحديد قاعدة ثابتة.

  • وضع ملفّ اختبار واحد لكلّ دالّة
  • إضافة .Tests.ps1 إلى اسم ملفّ الاختبار
  • توحيد طريقة تحميل الهدف المُختبَر
  • عدم خلط الاختبارات الوحدويّة بالاختبارات التكامليّة أكثر من اللازم

في البداية، يكفي وضع ملفّ .ps1 الهدف وملفّ الاختبار .Tests.ps1 جنباً إلى جنب.

5. تشغيل أصغر اختبار

أوّلاً، نُعِدّ دالّة بسيطة Get-OldLogFile.ps1.

function Get-OldLogFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [int] $Days = 30,

        [string] $Filter = '*.log',

        [datetime] $Now = (Get-Date)
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
        throw "Folder not found: $Path"
    }

    $limit = $Now.AddDays(-1 * $Days)

    Get-ChildItem -LiteralPath $Path -Filter $Filter -File |
        Where-Object { $_.LastWriteTime -lt $limit } |
        Sort-Object -Property LastWriteTime |
        Select-Object FullName, Name, Length, LastWriteTime
}

هنا، لتسهيل الاختبار، جعلنا $Now يُستقبَل كوسيط.

إن استُخدم Get-Date مباشرةً داخل الدالّة في كلّ مرّة، تتغيّر النتيجة حسب يوم تنفيذ الاختبار. أمّا إن جعلنا التاريخ وسيطاً، يمكن اختبار شرط ثابت مثل «الملفّات الأقدم من 30 يوماً حتّى تاريخ 1 يونيو 2026».

بعد ذلك، نكتب الاختبار في Get-OldLogFile.Tests.ps1.

BeforeAll {
    . $PSScriptRoot\Get-OldLogFile.ps1
}

Describe 'Get-OldLogFile' {
    BeforeEach {
        $script:Root = Join-Path $TestDrive 'logs'
        New-Item -ItemType Directory -Path $script:Root -Force | Out-Null

        $oldLog = Join-Path $script:Root 'old.log'
        $newLog = Join-Path $script:Root 'new.log'
        $oldTxt = Join-Path $script:Root 'old.txt'

        Set-Content -LiteralPath $oldLog -Value 'old log' -Encoding UTF8
        Set-Content -LiteralPath $newLog -Value 'new log' -Encoding UTF8
        Set-Content -LiteralPath $oldTxt -Value 'old text' -Encoding UTF8

        (Get-Item -LiteralPath $oldLog).LastWriteTime = [datetime]'2026-05-01T00:00:00'
        (Get-Item -LiteralPath $newLog).LastWriteTime = [datetime]'2026-05-31T00:00:00'
        (Get-Item -LiteralPath $oldTxt).LastWriteTime = [datetime]'2026-05-01T00:00:00'
    }

    It 'يُعيد ملفّات .log الأقدم من عدد الأيّام المحدَّد فقط' {
        $result = Get-OldLogFile `
            -Path $script:Root `
            -Days 30 `
            -Now ([datetime]'2026-06-01T00:00:00')

        $result | Should -HaveCount 1
        $result[0].Name | Should -Be 'old.log'
    }

    It 'يفشل عند عدم وجود المجلّد' {
        { Get-OldLogFile -Path (Join-Path $TestDrive 'missing') } |
            Should -Throw
    }
}

نُنفِّذه.

Invoke-Pester -Output Detailed .\Get-OldLogFile.Tests.ps1

$TestDrive المُستخدَم هنا هو منطقة مؤقّتة مخصَّصة للاختبار يوفّرها Pester. يمكن استخدام ملفّات أُنشئت داخل الاختبار فقط، دون استخدام C:\Logs الحقيقيّ أو مجلّد مشترَك. في سكربتات PowerShell التي تتضمّن عمليّات ملفّات، فإنّ اعتياد استخدام $TestDrive أوّلاً أمرٌ آمن.

6. اكتب أسماء الاختبارات كمواصفات (specification)

النصّ المكتوب في It الخاصّ بـ Pester ليس مجرّد وصف، بل يُشكِّل مواصفة صغيرة لمن يقرأه لاحقاً.

فمثلاً، اسمٌ كهذا ضعيف قليلاً.

It 'works' {
    # ...
}

لا يُفهَم منه ما الذي يُفترَض أن يعمل.

في العمل الفعليّ، وضع الشرط والنتيجة المتوقَّعة داخل الاسم يجعل القراءة أسهل.

It 'يُعيد ملفّات .log الأقدم من عدد الأيّام المحدَّد فقط' {
    # ...
}

It 'لا يستهدف الملفّ الذي يوافق يوم الانتهاء بالضبط' {
    # ...
}

It 'يفشل عند عدم وجود المجلّد' {
    # ...
}

أسماء الاختبارات الجيّدة تُفيد عند الفشل. حين يظهر هذا في سجلّ CI، تعرف فوراً ما الذي انكسر.

[-] Get-OldLogFile.لا يستهدف الملفّ الذي يوافق يوم الانتهاء بالضبط

اسم الاختبار هو مذكّرة موجَّهة إلى نفسك في المستقبل.

7. أضف شرطاً حدّيّاً واحداً

Get-OldLogFile السابقة تحكم على الملفّات القديمة بهذا الشرط.

$_.LastWriteTime -lt $limit

بسبب استخدام -lt، فإنّ الملفّ الذي له نفس تاريخ الانتهاء لا يُستهدَف.

هذا الحكم صغير، لكنّه مهمّ في العمل الفعليّ. لأنّ عدد الأهداف يتغيّر بحسب ما إذا كان المعنى «أقدم من 30 يوماً» أو «شامل ما قبل 30 يوماً».

نضيف الشرط الحدّيّ إلى الاختبار.

It 'لا يستهدف الملفّ الذي يوافق يوم الانتهاء بالضبط' {
    $border = Join-Path $script:Root 'border.log'
    Set-Content -LiteralPath $border -Value 'border log' -Encoding UTF8
    (Get-Item -LiteralPath $border).LastWriteTime = [datetime]'2026-05-02T00:00:00'

    $result = Get-OldLogFile `
        -Path $script:Root `
        -Days 30 `
        -Now ([datetime]'2026-06-01T00:00:00')

    $result.Name | Should -Not -Contain 'border.log'
}

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

8. ثبِّت شكل القيمة المُعادة

في سكربتات PowerShell، قد يتغيّر شكل القيمة المُعادة دون أن يُلاحَظ ذلك.

في البداية كانت تُعاد FileInfo كما هي. في مرحلة لاحقة أُضيف Select-Object. ثمّ تغيّرت أسماء الأعمدة لأجل CSV.

هذا النوع من التغييرات يؤثّر على المعالجة اللاحقة، لذا فإنّ اختبار خصائص القيمة المُعادة يساعد على ملاحظة التغييرات غير المتوقَّعة.

It 'يُعيد الخصائص المُستخدَمة في المعالجة اللاحقة' {
    $result = Get-OldLogFile `
        -Path $script:Root `
        -Days 30 `
        -Now ([datetime]'2026-06-01T00:00:00')

    $propertyNames = $result[0].PSObject.Properties.Name

    $propertyNames | Should -Contain 'FullName'
    $propertyNames | Should -Contain 'Name'
    $propertyNames | Should -Contain 'Length'
    $propertyNames | Should -Contain 'LastWriteTime'
}

في الدوال المُمرَّرة إلى إخراج CSV أو إعداد التقارير، أسماء الأعمدة نفسها مواصفة، لا القيم فقط.

لا نتحقّق فقط من أنّه «عمل»، بل من أنّ «القيمة عادت بالشكل الذي تتوقّعه المعالجة التالية».

9. افصل عمليّة الحذف عن اختيار الأهداف

بعد ذلك، نتناول عمليّة الحذف. نبدأ بمثال سيّئ.

Get-ChildItem C:\Logs -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
    Remove-Item -Force

قصيرة ومريحة، لكنّها صعبة الاختبار. لأنّ اختيار الهدف والحذف متّصلان في خطّ أنابيب (pipeline) واحد، يصعب معرفة ما ينبغي التحقّق منه.

في العمل الفعليّ، نفصلها هكذا:

function Remove-OldLogFile {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [int] $Days = 30,

        [datetime] $Now = (Get-Date)
    )

    $targets = Get-OldLogFile -Path $Path -Days $Days -Now $Now

    foreach ($target in $targets) {
        if ($PSCmdlet.ShouldProcess($target.FullName, 'Remove old log file')) {
            Remove-Item -LiteralPath $target.FullName -Force
        }
    }
}

هنا أضفنا SupportsShouldProcess لتمكين الدالّة من استقبال -WhatIf.

Remove-OldLogFile -Path C:\Logs -Days 30 -WhatIf

في دوال PowerShell الخاصّة بالحذف، من الأسلم قدر الإمكان جعلها قابلة للتجربة المسبقة (dry run) عبر -WhatIf.

10. استبدل العمليّات الخطِرة بـ Mock

في Pester، يمكن استخدام Mock لاستبدال تنفيذ الأمر الفعليّ.

لا حاجة إلى تنفيذ Remove-Item فعليّاً في اختبار عمليّة الحذف.

هل استُدعيت في الموضع الذي يجب أن تُستدعى فيه؟ وهل لم تُستدعَ في الموضع الذي لا يجب أن تُستدعى فيه؟

النظر في هذين يكفي.

مثال Remove-OldLogFile.Tests.ps1:

BeforeAll {
    . $PSScriptRoot\Get-OldLogFile.ps1
    . $PSScriptRoot\Remove-OldLogFile.ps1
}

Describe 'Remove-OldLogFile' {
    It 'يستدعي Remove-Item على ملفّات السجلّ القديمة' {
        Mock Get-OldLogFile {
            [pscustomobject]@{
                FullName      = 'C:\Logs\old.log'
                Name          = 'old.log'
                Length        = 10
                LastWriteTime = [datetime]'2026-05-01'
            }
        }

        Mock Remove-Item {}

        Remove-OldLogFile `
            -Path 'C:\Logs' `
            -Days 30 `
            -Now ([datetime]'2026-06-01')

        Should -Invoke Remove-Item `
            -Times 1 `
            -Exactly `
            -ParameterFilter { $LiteralPath -eq 'C:\Logs\old.log' }
    }

    It 'لا يستدعي Remove-Item عند استخدام WhatIf' {
        Mock Get-OldLogFile {
            [pscustomobject]@{
                FullName      = 'C:\Logs\old.log'
                Name          = 'old.log'
                Length        = 10
                LastWriteTime = [datetime]'2026-05-01'
            }
        }

        Mock Remove-Item {}

        Remove-OldLogFile `
            -Path 'C:\Logs' `
            -Days 30 `
            -Now ([datetime]'2026-06-01') `
            -WhatIf

        Should -Invoke Remove-Item -Times 0
    }
}

في هذا الاختبار، بما أنّنا نجعل Get-OldLogFile وRemove-Item كلاهما Mock، فلا حاجة لوجود C:\Logs\old.log الحقيقيّ أصلاً. ما نراقبه هو حكم Remove-OldLogFile نفسه:

  • استدعاء Remove-Item إن وُجد هدف
  • عدم استدعاء Remove-Item عند استخدام -WhatIf
  • تمرير المسار المقصود عند الاستدعاء

كلّما كانت العمليّة أخطر، كان اختبار شرط الاستدعاء بدل التنفيذ نفسه أكثر أماناً.

11. لا تُفرِط في استخدام Mock

Mock مفيدة، لكنّ الإفراط في استخدامها يُنقِص قيمة الاختبار. فجعل كلّ شيء Mock يُبعِدك كثيراً عن سلوك PowerShell الفعليّ.

هذا تقريباً هو المعيار:

العمليّة التوصية
التاريخ تثبيته عبر وسيط
إنشاء الملفّات استخدام $TestDrive
الحذف/النقل التحقّق عبر Mock و-WhatIf
استدعاء Web API استخدام Mock على Invoke-RestMethod وما شابه
إرسال البريد/الإشعارات استخدام Mock على أمر الإرسال
قراءة/كتابة CSV إنشاء ملفّات حقيقيّة صغيرة في $TestDrive

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

في المقابل، العمليّات مثل الحذف والإشعار وواجهات API الخارجيّة وإيقاف الخدمات، من الأفضل عدم تنفيذها فعليّاً.

افصل «مكان استخدام الشيء الحقيقيّ» عن «مكان استخدام Mock».

12. عدِّل السكربت القائم ليصبح قابلاً للاختبار بسهولة

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

قبل التعديل

$limit = (Get-Date).AddDays(-30)

Get-ChildItem C:\Logs -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt $limit } |
    Remove-Item -Force

بعد التعديل

function Get-OldLogFile {
    param(
        [string] $Path,
        [int] $Days = 30,
        [datetime] $Now = (Get-Date)
    )

    $limit = $Now.AddDays(-1 * $Days)

    Get-ChildItem -LiteralPath $Path -Filter *.log -File |
        Where-Object { $_.LastWriteTime -lt $limit }
}

function Remove-OldLogFile {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string] $Path,
        [int] $Days = 30,
        [datetime] $Now = (Get-Date)
    )

    Get-OldLogFile -Path $Path -Days $Days -Now $Now |
        ForEach-Object {
            if ($PSCmdlet.ShouldProcess($_.FullName, 'Remove old log file')) {
                Remove-Item -LiteralPath $_.FullName -Force
            }
        }
}

التغييرات ليست كبيرة.

  • جعل التاريخ وسيطاً
  • جعل اختيار الأهداف دالّة
  • فصل عمليّة الحذف في دالّة منفصلة
  • إضافة SupportsShouldProcess

هذا وحده يجعل الاختبار أسهل بكثير.

في تهيئة اختبار PowerShell، من الأنجع جعل «التاريخ» و«المسار» و«الأوامر الخارجيّة» و«عمليّات التغيير» قابلة للاستبدال من الخارج، بدل الدخول من نظريّة التصميم أوّلاً.

13. حدِّد تصنيف الاختبارات

في Pester، يمكن إضافة وسوم (tags) إلى Describe وContext وIt.

فمثلاً، نفصل بين اختبار وحدويّ سريع واختبار تكامليّ يتعامل مع البيئة الفعليّة.

Describe 'Get-OldLogFile' -Tag 'Unit' {
    It 'يُعيد ملفّات .log الأقدم من عدد الأيّام المحدَّد فقط' {
        # اختبار سريع يستخدم TestDrive
    }
}

Describe 'Log maintenance smoke test' -Tag 'Smoke' {
    It 'يستطيع قراءة مجلّد السجلّات الفعليّ' {
        Test-Path -LiteralPath 'C:\Logs' | Should -BeTrue
    }
}

نُنفِّذ الاختبارات الوحدويّة فقط.

Invoke-Pester -TagFilter Unit

نستبعد الاختبارات البطيئة أو المعتمِدة على البيئة.

Invoke-Pester -ExcludeTagFilter Slow, RequiresAdmin, Network

في العمل الفعليّ، محاولة تنفيذ كلّ الاختبارات في كلّ مرّة قد لا تستمرّ.

اجعل الاختبارات السريعة الخالية من الآثار الجانبيّة (side effects) هي المعيار أوّلاً، وافصل الاختبارات المعتمِدة على البيئة عبر الوسوم لتنفيذها عند الحاجة.

14. التشغيل عبر CI

Pester مفيد حتّى عند التنفيذ اليدويّ، لكن إن كان الفريق يدير السكربتات، فإنّ إعداد إمكانيّة التنفيذ عبر CI يمنح طمأنينة إضافيّة.

فمثلاً، نُعِدّ ملفّاً مثل tools/Invoke-ProjectTests.ps1.

$ErrorActionPreference = 'Stop'

Import-Module Pester

$config = New-PesterConfiguration

$config.Run.Path = @(
    Join-Path $PSScriptRoot '..\tests'
)

$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'

$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'JUnitXml'
$config.TestResult.OutputPath = Join-Path $PSScriptRoot '..\test-results.xml'

$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = @(
    Join-Path $PSScriptRoot '..\src'
)
$config.CodeCoverage.OutputPath = Join-Path $PSScriptRoot '..\coverage.xml'

Invoke-Pester -Configuration $config

نُنفِّذ هذا السكربت من جهة CI.

pwsh -NoProfile -File .\tools\Invoke-ProjectTests.ps1

المهمّ هو عدم كتابة إعدادات خاصّة بـ CI داخل ملفّ الاختبار نفسه أكثر من اللازم.

ملفّ الاختبار مكان لكتابة المواصفات. أمّا صيغة المُخرَجات الخاصّة بـ CI، والتغطية (coverage)، ورمز الخروج (exit code) وما شابه، فتصبح الأمور أوضح عند تجميعها في سكربت التنفيذ.

15. انظر إلى التغطية كخريطة لا كهدف

يمكن لـ Pester إخراج تغطية الشيفرة (code coverage) أيضاً. لكن من الأفضل عدم ملاحقة الأرقام بإفراط منذ البداية. لأنّ التغطية ليست جودة الاختبار نفسها.

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

استخدم التغطية على هذا النحو:

  • إيجاد الدوال التي لم تُختبَر إطلاقاً
  • إيجاد الفروع (branches) المهمّة التي لا اختبار لها
  • ترتيب الأولويّات من السكربتات ذات التغيير المتكرِّر
  • ترك أثر لتنفيذ الاختبار في CI

انظر إلى «هل الأحكام المهمّة مُختبَرة» بدل رفع الأرقام.

16. ترتيب إدخال الاختبار على السكربتات القائمة

عند إدخال Pester على أصول PowerShell القائمة، من الأسهل عدم جعل الكلّ هدفاً للاختبار دفعةً واحدة. نذكر الترتيب المُوصى به.

1. اختر السكربت الذي يسبِّب مشكلة عند فشله

في البداية، تناسب سكربتات كهذه:

  • تتضمّن حذفاً أو نقلاً أو استبدالاً (overwrite)
  • تعمل يوميّاً أو شهريّاً
  • موجودة في دليل الإجراءات لكنّها مرتبطة بشخص معيّن (attributed to one person)
  • وقع فيها خطأ في الشروط سابقاً
  • CSV المُخرَج منها يُستخدَم في عمل آخر

ابدأ بما هو مفيد لكن يسبِّب مشكلة عند كسره.

2. استخلص جزء القراءة فقط كدالّة

أوّل ما نختبره ليس عمليّة التغيير، بل عمليّة القراءة.

قراءة السجلّ
تضييق الهدف
عدّ العناصر
تهيئة الشكل لـ CSV

هذا الجزء يسهل اختباره عبر $TestDrive، وقلّما يقع فيه حادث.

3. مرِّر التاريخ والمسار من الخارج

إن كانا ثابتَين، يصعب الاختبار.

# تجنَّب هذا
$root = 'C:\Logs'
$limit = (Get-Date).AddDays(-30)

الشكل السهل الاختبار:

param(
    [string] $Path,
    [datetime] $Now = (Get-Date)
)

مجرّد إمكانيّة تمرير القيمة من الخارج يرفع استقرار الاختبار بشكل كبير.

4. اجعل العمليّة الخطِرة في النهاية

اجمع الحذف والنقل في النهاية.

إنشاء الهدف
  ↓
تسجيل الهدف في السجلّ
  ↓
التحقّق عبر -WhatIf
  ↓
التنفيذ

نتحقّق بنفس الترتيب في الاختبار أيضاً.

17. الأعطال الشائعة

العرض السبب المعالجة
ينجح محلّيّاً لكن يفشل في CI اختلاف المجلّد الحاليّ جعله مبنيّاً على $PSScriptRoot
تتغيّر النتيجة من يوم لآخر استخدام Get-Date مباشرةً إعداد وسيط مثل -Now
على وشك حذف ملفّ حقيقيّ في الاختبار استخدام مجلّد حقيقيّ استخدام $TestDrive وMock
لا يعمل Mock اختلاف حدود الوحدة أو النطاق التحقّق من -ModuleName أو طريقة التحميل
لا يُعرَف حتّى أين يجب الاختبار المواصفة غير مقسَّمة إلى دوالّ فصلها إلى اختيار الأهداف، والتهيئة، وعمليّة التغيير
الاختبار بطيء يتّصل بخدمة خارجيّة أو شبكة جعل الاعتماديّات الخارجيّة Mock في الاختبار الوحدويّ
لا يُفهَم شيء من اسم الاختبار اسم مثل It 'works' وضع الشرط والنتيجة المتوقَّعة في الاسم

كثيراً ما يبدو الأمر مشكلة في Pester بينما السبب الفعليّ هو بنية السكربت.

الجزء الذي يصعب اختباره هو غالباً نفس الجزء الذي يسهل كسره في التشغيل الفعليّ.

18. القواعد التي ينبغي تحديدها في تهيئة الاختبار

عند إدارة سكربتات PowerShell ضمن فريق، حدِّد القواعد قبل تفاصيل الكتابة.

مثال على هذه القواعد:

  • جعل ملفّات الاختبار بصيغة *.Tests.ps1
  • تحميل الهدف المُختبَر من $PSScriptRoot
  • استخدام $TestDrive في اختبار عمليّات الملفّات
  • جعل الحذف والنقل والإشعار واستدعاء API كلّها Mock من حيث المبدأ
  • جعل التاريخ قابلاً للتثبيت عبر وسيط
  • إضافة وسوم مثل Unit وSmoke وRequiresAdmin إلى Describe أو It
  • تنفيذ Unit كمعيار في CI
  • إضافة SupportsShouldProcess قدر الإمكان لدوالّ التغيير
  • الإبقاء على الأعطال السابقة كاختبارات لمنع التكرار

القواعد الكثيرة جدّاً لا تُحتَرم. في البداية، تكفي هذه الثلاث فقط.

استخدام TestDrive
تثبيت التاريخ
جعل العمليّات الخطِرة Mock

مجرّد الالتزام بهذه الثلاث يجعل اختبار PowerShell مستقرّاً إلى حدٍّ كبير.

19. حدِّد أيضاً ما لا تختبره

في تهيئة الاختبار، تحديد «ما لا نختبره» مهمّ بقدر تحديد «ما نختبره».

فمثلاً، هذه الأمور من الأفضل عدم التحقّق منها بإفراط في الاختبار الوحدويّ:

  • أنّ Get-ChildItem الخاصّ بـ Windows نفسه يعمل بشكل صحيح
  • أنّ Remove-Item يحذف الملفّ فعلاً
  • المواصفات الداخليّة لأوامر PowerShell القياسيّة
  • أنّ واجهة API خارج الشركة تستجيب دائماً
  • أنّ المجلّد المشترَك عبر الشبكة متاح دائماً

ما ينبغي اختباره هو أحكامنا نحن.

  • أيّ شرط يجعل العنصر هدفاً
  • أيّ مسار يُمرَّر
  • أيّ عمود يُخرَج
  • كيف يُعامَل عند الفشل
  • هل يمكن تجربة العمليّة الخطِرة مسبقاً

افصل بين ما تثق به من الأوامر القياسيّة، وما تحمي به منطقك الخاصّ.

20. خلاصة

PowerShell أداة مريحة تُتيح أتمتة العمل اليوميّ بسرعة. لكنّ السكربتات المُستخدَمة طويلاً في العمل الفعليّ تتحمّل مسؤوليّة تزداد شيئاً فشيئاً. ما كان في البداية أمراً من سطر واحد يستخدمه صاحبه فقط، يصبح في النهاية عمليّة تشغيل يوميّة تؤثّر على عمل الآخرين وبياناتهم.

تهيئة الاختبار عبر Pester هي العمل الذي يحمي السكربت مع هذا التحوّل.

نُلخِّص النقاط الأساسيّة:

  • ابدأ باختبار اختيار الأهداف أوّلاً
  • اجعل التاريخ والمسار قابلَين للتمرير من الخارج
  • احصر عمليّات الملفّات داخل $TestDrive
  • اجعل الحذف والنقل والإشعار واستدعاء API كلّها Mock
  • اجعل دوالّ التغيير قابلة للتجربة المسبقة عبر -WhatIf
  • اجعل اسم الاختبار قابلاً للقراءة كمواصفة
  • شغِّل في CI الاختبارات السريعة الخالية من الآثار الجانبيّة أوّلاً

التشغيل الآمن لـ PowerShell لا يعني إدخال آليّة كبيرة دفعةً واحدة.

التقسيم إلى دوالّ صغيرة. كتابة اختبارات صغيرة. جعل الأمر قابلاً للتحقّق قبل العمليّة الخطِرة.

بهذا التراكم، يقترب PowerShell من كونه «سكربتاً مريحاً لكن مخيفاً قليلاً» إلى «أداة عمل يمكن التحقّق منها حتّى بعد التعديل».

روابط مرجعيّة

مقالات ذات صلة

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

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

أسئلة شائعة حول موضوع هذه المقالة.

ما الذي ينبغي اختباره أوّلاً عبر Pester؟
لا تبدأ بعمليّة الحذف نفسها، بل بالعمليّة التي تختار أهداف الحذف (اختيار الأهداف). في سكربتات التشغيل، إعطاء الأولويّة لأربعة أمور يُظهر أثراً سريعاً: «فحص الشروط»، و«شكل المُخرَجات»، و«ما قبل العمليّات الخطِرة»، و«الاعتماديّات الخارجيّة». لا حاجة إلى إعادة هيكلة السكربت القائم دفعةً واحدة، بل ابدأ باستخلاص جزء الحكم الذي يسبق العمليّة الخطِرة كدالّة، وتحقّق من قيمتها المُعادة عبر Pester.
ما هو TestDrive في Pester؟
$TestDrive هو منطقة مؤقّتة مخصَّصة للاختبار يوفّرها Pester. يتيح لك التحقّق من عمليّات الملفّات باستخدام ملفّات أُنشئت داخل الاختبار فقط، دون استخدام C:\Logs الحقيقيّ أو مجلّد مشترَك. في اختبار سكربتات PowerShell التي تتضمّن عمليّات ملفّات، فإنّ اعتياد استخدام $TestDrive أوّلاً يمنع حوادث مثل حذف ملفّات حقيقيّة بالخطأ.
إلى أيّ مدى ينبغي استخدام Mock في Pester؟
العمليّات التي لا يجوز تنفيذها فعليّاً، مثل الحذف والنقل، واستدعاء واجهات Web API، وإرسال البريد الإلكترونيّ أو الإشعارات، يُستحسن استبدالها بـ Mock، أمّا التاريخ فيُثبَّت عبر وسيط (argument)، وإنشاء الملفّات وقراءة/كتابة CSV فيُفضَّل إنشاء ملفّات حقيقيّة صغيرة داخل $TestDrive. جعل كلّ شيء Mock يُبعِدك كثيراً عن سلوك PowerShell الفعليّ، فقد تفوتك مشكلات ترميز الأحرف أو فواصل الأسطر أو أسماء الأعمدة، لذا فإنّ النقطة الأساسيّة هي الفصل بين «مكان استخدام الشيء الحقيقيّ» و«مكان استخدام Mock».
لماذا تنجح اختبارات Pester على الجهاز المحلّي لكنّها تفشل في CI؟
السبب الشائع هو اختلاف المجلّد الحاليّ (current directory)، ويُحلّ بجعل تحميل الهدف مبنيّاً على $PSScriptRoot. إن كانت النتيجة تختلف من يوم لآخر فالسبب هو استخدام Get-Date مباشرةً، لذا ثبِّت التاريخ عبر وسيط مثل -Now. إن لم يعمل Mock فاشتبه في حدود الوحدة (module) أو نطاقها (scope)، وتحقّق من -ModuleName أو طريقة التحميل. كثيراً ما يبدو الأمر مشكلة في Pester بينما السبب الفعليّ هو بنية السكربت.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

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

روابط عامة

العودة إلى المدونة