בנה מסגרת בדיקות PowerShell Pester הנמדדת

אם מוצאים את עצמכם מבלים יותר זמן בתחזוקת הבדיקות של Pester מאשר ביצירת בדיקות חדשות, הפוסט הזה בשבילכם. בפוסט זה, אשתף פרויקט שעליו עבדתי יחד עם Devolutions.

רקע

היה לנו בעיה ב- Devolutions. מודול הפווארשל המוביל Devolutions.PowerShell היה חסר בדיקות Pester. אני יודע, ידעתי שיש להם בדיקות C# אך זו סיפור אחר.

כיוון שאני יועץ עבור Devolutions שמתמקד ב-PowerShell, נדרשתי ליצור סדרה של בדיקות Pester שישמשו בצינור CI/CD וירוצו לפני ההפצה לייצור. לא הייתה בעיה, חשבתי. השתמשתי במודול מספר פעמים, וזה לא יכול להיות כל כך קשה. טעיתי.

2025-01-29_09-27-59.png

בלעדי בקשה ספציפית, רק "לבנות כמה בדיקות", התחלתי לבנות בדיקות עבור כל המודול, רק כדי לגלות שיש בו כמעט 500 פקודות! זה היה לקח זמן.

מאחר ותמיד רציתי לבנות פתרונות לא רק להיום אלא גם לטווח הארוך, לא רציתי לזרוק קוד בדיקות בודדות לתוך סקריפט אחד ולקרוא לזה יום. הסקריפט הזה היה יכול להיות עצום!

במקום זאת, לפני שכתבתי בדיקה אחת, רציתי לעצב מסגרת שאוכל להשתמש בה שת:

  1. תהיה נמלצת – אני או אחרים יוכלו להוסיף בדיקות למסגרת מבלי לחשוב מדי.
  2. כדי למנוע כפילות קוד, הפרויקט צריך להיות DRY (אל תחזור על עצמך). לא רציתי לשכפל קוד, ובמקום זאת השתמשתי בסקריפטים משותפים, פונקציות, וכדומה.
  3. כדי להשתמש במבחנים המבוססים על נתונים: מבחנים שמבוססים על נתונים תומכים בשימוש חוזר בקוד על ידי הגדרת מבחנים במבנה נתונים ואז קישור המבחנים למבנה זה במקום לשכפל קוד בדיקה עבור כל מבחן.
  4. להיות מודולרי – כל המבחנים חייבים להיפרד למבנה שימנע צורך לגלול דרך קוד נוסף כדי למצוא את מה שאתה מחפש.
  5. כדי לתמוך בהגדרת תרגום סביבה וסגירת תרגום – מבחנים אלה הם מבחנים סופיים (E2E) / אינטגרציה / אישור, כל מה שתרצה. הם בודקים את שימוש המשתמש הסופי; אין בדיקות יחידה, ולכן עליהם להכיל מגוון דברים בסביבה כדי לרוץ.
  6. כדי לתמוך בהפעלת קבוצות של מבחנים מבלי להפעיל אותם כולם בו זמנית.

דרישות אלה מובילות למסגרת הבדיקה Pester עבור Devolutions.

תנאי קדם

אם ברצונך להשתמש במסגרת זו, אני לא יכול להבטיח ב-100% שהיא תעבוד אלא אם תעבוד באותה סביבה שאני. כדי להשתמש במסגרת זו, השתמשתי ב:

  • PowerShell v7.4.5 ב-Windows
  • Pester 5.6.1

זה עדיין יעבוד ב- Windows PowerShell ובגרסאות קודמות של Pester v5, אך אין הבטחות!

סקירת המסגרת

המסגרת הזו מורכבת מארבעה רכיבים: סקריפט קורא, סקריפטי הגדרת מבחנים שונים, סקריפט אופציונלי לאחסון פונקציות עזר, וסקריפטים לפני הכל / לפני כל אופציונליים. כל אחד מהרכיבים אלה מופנה במערכת הקבצים כך:

📁 root/
   📄 caller.tests.ps1
   📄 helper_functions.ps1
   📁 test_definitions/
      📁 group1/
         📄 beforeall.tests.ps1
         📄 beforeeach.tests.ps1
         📄 core.tests.ps1
         📄 subgroup.tests.ps1

כאשר אתה קורא לסקריפט הקורא:

Invoke-Pester -Path root/caller.tests.ps1

הסקריפט הקורא:

  1. מוצא את כל סקריפטי ההגדרות למבחנים בכל קבוצה.
  2. מוצא את כל סקריפטי ה־beforeeach וה־beforeall עבור כל קבוצה.
  3. יוצר קונטקסטים של Pester עבור כל קבוצה ותת־קבוצה.
    context 'קבוצה' {
        context 'תת־קבוצה' {
    
        }
    }
  4. קורא לסקריפט ה־beforeall פעם אחת לפני ריצת כל מבחן בקבוצה.
  5. קורא לסקריפט ה־beforeeach לפני כל מבחן בקבוצה.
  6. לבסוף, זה קורא לאמות הבדיקה שמוגדרות בכל הגדרת מבחן.

הסקריפט הבודק

הסקריפט הבודק הוא נקודת ההפעלה של Pester. זהו הסקריפט שה־Invoke-Pester קורא כאשר נדרש להפעיל את כל המבחנים במערכת.

הסקריפט הבודק מחולק ל־X אזורים:

הבלוק לפני הגילוי

משימה אחת של הסקריפט הבודק היא למצוא את כל ההגדרות למבחנים ומה גלויות בשלב הגילוי של Pester. בתוך בלוק ה־BeforeDiscovery של הסקריפט, נאספות ההגדרות למבחנים, כמו גם כל סקריפטי ה־before each או ה־before all.

BeforeDiscovery {

    # Initialize hashtable to store all test definitions
    $tests = @{}

    # Iterate through test group directories (e.g. datasources, entries)
    Get-ChildItem -Path "$PSScriptRoot\test_definitions" -Directory -PipelineVariable testGroupDir | ForEach-Object {
        $testGroup = $testGroupDir.BaseName
        $tests[$testGroup] = @{}

        # Load beforeall script for test group if it exists
        # This script runs once before all tests in the group
        $testGroupBeforeAllScriptPath = Join-Path -Path $testGroupDir.FullName -ChildPath 'beforeall.ps1'
        if (Test-Path -Path $testGroupBeforeAllScriptPath) {
            $tests[$testGroup]['beforeall'] += . $testGroupBeforeAllScriptPath
        }

        # Load beforeeach script for test group if it exists
        # This script runs before each individual test in the group
        $testGroupBeforeEachScriptPath = Join-Path -Path $testGroupDir.FullName -ChildPath 'beforeeach.ps1'
        if (Test-Path -Path $testGroupBeforeEachScriptPath) {
            $tests[$testGroup]['beforeeach'] += . $testGroupBeforeEachScriptPath
        }

        # Load all test definition files in the group directory
        Get-ChildItem -Path $testGroupDir.FullName -Filter '*.ps1' -PipelineVariable testDefinitionFile | ForEach-Object {
            $tests[$testGroup][$testDefinitionFile.BaseName] = @()
            $tests[$testGroup][$testDefinitionFile.BaseName] += . $testDefinitionFile.FullName
        }
    }
}

הבלוק ה־BeforeAll

הבלוק ה־BeforeAll רץ כדי להפוך פונקציות עוזר לסקריפט הבודק או להגדרות המבחנים לזמינות. ב־Pester v5, לא ניתן לבצע משימה זו ב־BeforeDiscovery; אחרת, לא הייתה תקפה למבחנים.

# Load helper functions used across all tests
BeforeAll {
    . (Join-Path $PSScriptRoot -ChildPath "_helper_functions.ps1")
}

הבלוקים Describe ו־Contexts

כל תצורת סביבת שאליה יש צורך להריץ בדיקות נפרדת מחולקת לקונטקסטים עם קבוצות מבחנים מתחת לכל אחד מהם.

בשביל מודול הפווארשל של Devolutions, מאחר ואנו צריכים לבדוק cmdlets נגד מגוון רחב של מקורות נתונים, יצרתי קשורים לפי מקור הנתונים, אך תוכלו להשתמש בכל דבר כאן. לאחר מכן, באמצעות פרמטר ForEach של Pester, כל תיקיית הגדרת בדיקה היא קשור, כמו גם כל קבוצה משנית. לאחר מכן, כל בדיקה מוגדרת מתחת כבלוקים it.


שימו לב לשימוש בשיטת where() על בלוק ה־it. Where({ $_.environments -contains 'xxx' -or $_.environments.count -eq 0}). זהו המקום בו מתבצעת מבנה של כל ההגדרה. זהו המקום בו אנו מציינים אילו בדיקות ירוצו באילו סביבות.


# Main test container for all tests
Describe 'RDM' {

    # Tests that run against an environment
    Context 'Environment1' -Tag 'Environment1' {

        BeforeEach {
            ## Do stuff to execute before each test in this content. For example, this is used to set up a specific data source for Devolutions Remote Desktop Manager
        }

        # Clean up
        AfterAll {

        }

        # Run each test group against the environment
        Context '<_>' -ForEach $tests.Keys -Tag ($_) {
            $testGroup = $_

            # Run each test subgroup (core, properties, etc)
            Context '<_>' -ForEach $tests[$testGroup].Keys -Tag ($_) {
                $testSubGroup = $_

                # Run tests marked for this environment or all
                It '<name>' -ForEach ($tests[$testGroup][$testSubGroup]).Where({ $_.environments -contains 'xxx' -or $_.environments.count -eq 0}) {
                    & $_.assertion
                }
            }
        }
    }

    ## Other environments. If you have specific configurations some tests must have, you can define them here by creating more context blocks.
}

הגדרות הבדיקות

לאחר מכן, יש לכם את החלק הכי חיוני: הבדיקות! הבדיקות נוצרות בתת־קבוצות (subgroup.tests.ps1) בתוך כל תיקיית קבוצה ויש ליצור אותן כמערך של טבלאות עם המבנה הבא:

@(
    @{
        'name' = 'creates a thing'
        'environments' = @() ## Nothing means all environments
        'assertion' = {
            ## Teating something
            $true | should -Be $true
        }
    }
)

כאן, תוכלו להגדיר את הסביבות בתסריט הקורא לביצוע התסריטים. לדוגמה, אם יש לך סביבה עבור כל מקור נתונים שהשתמשתי בו עבור Devolutions RDM, הסביבות שלי הן xml, sqllite, וכו'.

פונקציות עזר

לבסוף, יש לנו את תסריט הפונקציות העזר. תסריט זה מכיל פונקציות שאנו מפעילים בבלוק ה־BeforeAll בתסריט הקורא. זהו המקום בו תוכלו לשים פונקציות שתשתמשו בהן מחדש. בדוגמתי, יש לי פונקציות להגדרת מקורות נתונים והסרת כולם.

# Helper function to remove all entries from the current data source
# Used for cleanup in test scenarios
function Remove-AllEntries {
    try {
        # Get all entries and their IDs
        # Using ErrorAction SilentlyContinue to handle case where no entries exist
        $entries = @(Get-RDMEntry -ErrorAction SilentlyContinue)

        # If no entries found, just return silently
        # No cleanup needed in this case
        if ($entries.Count -eq 0) {
            return
        }

        # Delete entries one at a time
        # Using foreach loop to handle errors for individual entries
        foreach ($entry in $entries) {
            try {
                # Remove entry and refresh to ensure UI is updated
                Remove-RDMEntry -ID $entry.ID -Refresh -ErrorAction Stop
            } catch [System.Management.Automation.ItemNotFoundException] {
                # Silently ignore if entry is already gone
                # This can happen if entry was deleted by another process
                continue
            }
        }
    } catch {
        # Only warn about unexpected errors
        # Ignore "Connection not found" as this is expected in some cases
        if ($_.Exception.Message -ne 'Connection not found.') {
            Write-Warning "Error during cleanup: $_"
        }
    }
}

# Helper function to remove all data sources and their files
# Used for cleanup in test scenarios
function Remove-AllDatasources {
    # Remove any data sources currently loaded in memory
    # This ensures clean state for tests
    Get-RDMDataSource | ForEach-Object { Remove-RDMDataSource -DataSource $_ }

    # Delete any existing data source folders on disk
    # These are identified by GUIDs in the RDM application data folder
    Get-ChildItem $env:LOCALAPPDATA\Devolutions\RemoteDesktopManager | 
        Where-Object { $_.Name -match '^[0-9a-fA-F\-]{36}
}

איך לבנות את הפריימוורק שלך

האם הפריימוורק הזה נראה שימושי? האם אתה חושב שזה יהיה מועיל לארגונך? אם כן, כאן כמה טיפים ושאלות לחשוב עליהן.

  1. כמה בדיקות אתה מעריך שיהיו נדרשות? האם המקרה השימוש שלך מספיק לתמוך בפריימוורק הזה במקום פשוט כמה סקריפטים לבד?
  2. לפני כתיבת כל פוורשל, תאר כיצד אתה מעריך להגדיר קבוצות ותת-קבוצות. יצורם כמופשט הגיוני של האיברים שאתה בודק.
  3. חשוב על סביבות והתצורה שתצטרך לכל סט של בדיקות; צור אותן כהקשרים בתסריט הקורא.
  4. כשאתה בונה את ההגדרות לבדיקה, אם אתה מוצא את עצמך חוזר על קוד, צור פונקציה עבור זה והנחה אותה ב־helper_functions.ps1.

לבניית פריימוורק ניסיוני של Pester נדרש תכנון וארגון זהיר, אך היתרונות שווים את המאמץ. על ידי עקיבה אחרי המבנה שנתונה במאמר זה – עם עיצוב המודולרי, רכיבים הניתנים לשימוש חוזר, וטיפול גמיש בסביבות – אתה יכול ליצור פתרון לבדיקות חזק שגודל עם הצרכים שלך. בין אם אתה בודק מודולים של PowerShell, הגדרות תשתיות, או אפליקציות מורכבות, הפריימוורק הזה מספק בסיס יציב לשמירה על איכות קוד ואמינות על פני הארגון שלך.

Source:
https://adamtheautomator.com/powershell-pester-testing-framework/