iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
IT Operation

系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server系列 第 25 篇

Day 25|PowerShell 測試與防呆:怎麼知道修改 Module 後沒有把原本功能弄壞?

  • 分享至 

  • xImage
  •  

Day 24 我們把前面累積的 Function:
Write-Log
Get-ServerHealth
Get-HealthEventType
Save-HealthState

開始整理成自己的:
SysAdminToolkit

最後可以:
Import-Module SysAdminToolkit

直接使用:
Get-ServerHealth

這代表我們的 PowerShell 已經從:
單一 Script

慢慢變成:
可重複使用的 Tool

但接下來會出現一個非常實際的問題。
假設現在:
SysAdminToolkit 1.0.0

跑得很好。
某一天我修改:
Get-HealthEventType

想增加新的判斷。
原本:
Previous = Warning
Current = Critical

Expected

Escalated

結果程式改完之後,不小心變成:
Changed

如果我只是看著程式碼說:
看起來應該沒問題。

然後直接丟進排程,
可能隔天才發現:
Critical 沒有正確 Escalate
Notification 邏輯失效
State 判斷錯誤

所以今天要開始處理另一個非常重要的問題:
Testing
也就是:
怎麼證明修改程式之後,原本的功能還是正常?

今天會從最基本的:
Input
↓
Function
↓
Actual Result
↓
Expected Result
↓
Pass / Fail

開始。
最後再介紹 PowerShell 很重要的測試框架:
Pester
為什麼維運 Script 也需要測試?
很多人提到:
Unit Test
Integration Test
Automated Test

第一個想到的是:
Software Developer

但其實維運 Automation 一樣需要。
甚至有些情況更需要。
因為你的 Script 可能操作:
Active Directory
Windows Server
帳號權限
Service
File Server
Task Scheduler

如果程式寫錯,
影響的不是:
網頁按鈕顏色錯了

而可能是:
帳號判斷錯誤
權限處理錯誤
Server Status 判斷錯誤
錯誤通知沒有發出

尤其我們前面已經做到:
Task Scheduler
↓
凌晨自動執行

沒有人在旁邊看。
所以 Testing 開始變得非常重要。
最簡單的測試其實我們早就在做
假設:
Get-HealthEventType -PreviousStatus "Healthy"
-CurrentStatus "Critical"

得到:
NewAlert

我們心裡其實會做:
Expected

NewAlert

Actual

NewAlert

所以
Pass

這本身就是測試。
只是目前:
靠人眼檢查。

先做最簡單的手動 Test
例如:
$Expected =
"NewAlert"

$Actual =
Get-HealthEventType -PreviousStatus "Healthy"
-CurrentStatus "Critical"

然後:
if ($Actual -eq $Expected) {

Write-Host "PASS"

}
else {

Write-Host "FAIL"

}

結果:
PASS

這已經是一個:
Automated Assertion

Assertion 是什麼?
簡單理解:
我預期某件事情應該成立

例如:
1 + 1 應該 = 2

對我們的 Module:
Healthy → Critical
應該 = NewAlert

所以:
Expected

NewAlert

如果:
Actual

NewAlert

測試通過。
如果:
Actual

Changed

測試失敗。
為什麼不要只測一個 Case?
因為 Get-HealthEventType 不只有:
Healthy → Critical

還有很多 Transition。
例如:
Healthy → Healthy
Critical → Critical
Critical → Healthy
Warning → Critical
Critical → Warning
Healthy → Unknown
Unknown → Healthy

假設我們只測:
Healthy → Critical

它正常,
不代表:
Critical → Healthy

沒有被改壞。
建立 Test Cases
例如:
$TestCases = @(

@{
    Previous = "Healthy"
    Current  = "Critical"
    Expected = "NewAlert"
}

@{
    Previous = "Critical"
    Current  = "Critical"
    Expected = "ExistingIssue"
}

@{
    Previous = "Critical"
    Current  = "Healthy"
    Expected = "Recovery"
}

@{
    Previous = "Warning"
    Current  = "Critical"
    Expected = "Escalated"
}

@{
    Previous = "Critical"
    Current  = "Warning"
    Expected = "Improved"
}

)

然後:
foreach ($Case in $TestCases) {

$Actual =
    Get-HealthEventType `
        -PreviousStatus $Case.Previous `
        -CurrentStatus $Case.Current


if (
    $Actual -eq
    $Case.Expected
) {

    Write-Host `
        "PASS: $($Case.Previous) -> $($Case.Current)"

}
else {

    Write-Host `
        "FAIL: $($Case.Previous) -> $($Case.Current)"

    Write-Host `
        "Expected: $($Case.Expected)"

    Write-Host `
        "Actual:   $Actual"
}

}

可能:
PASS: Healthy -> Critical
PASS: Critical -> Critical
PASS: Critical -> Healthy
PASS: Warning -> Critical
PASS: Critical -> Warning

這已經比:
修改完之後自己點幾次看看

可靠很多。
但自己寫 Test Framework 很快會變麻煩
接下來可能又需要:
統計幾個 Pass
幾個 Fail
顯示哪一個失敗
支援 Before / After
Mock 外部 Command
產生 Test Result
CI/CD 整合

最後很可能:
為了測試 PowerShell,我又寫了一套測試系統。

所以 PowerShell 已經有成熟的 Testing Framework:
Pester
Pester 是什麼?
Pester 是 PowerShell 常用的測試框架。
概念上可以寫:
Describe "Get-HealthEventType" {

It "Healthy to Critical should return NewAlert" {

    $Result =
        Get-HealthEventType `
            -PreviousStatus "Healthy" `
            -CurrentStatus "Critical"

    $Result |
    Should -Be "NewAlert"
}

}

閱讀起來甚至很像英文:
Describe
Get-HealthEventType

It
Healthy to Critical should return NewAlert

然後:
Actual Result
Should Be
NewAlert

先確認 Pester
可以:
Get-Module -ListAvailable
Pester |
Sort-Object Version -Descending |
Select-Object `
Name,
Version,
Path

如果環境已有:
Name Version


Pester 5.x.x

就可以開始使用。
不同 Windows / PowerShell 環境可能帶有不同版本的 Pester,所以正式寫測試前最好先確認版本。
如果沒有 Pester
一般環境可以透過 PowerShell Gallery 安裝,例如:
Install-Module Pester
-Scope CurrentUser

如果是企業受管控電腦,可能不能直接連 PowerShell Gallery。
這時候應該依公司的:
Internal Repository
Package Policy
Software Deployment

方式安裝。
不要為了裝測試工具去繞過公司的安全政策。
建立 Tests 資料夾
Day 24 的專案:
SysAdmin-Automation/
│
├── Modules/
├── Config/
├── Scripts/
├── Reports/
├── Logs/
└── State/

今天加入:
Tests/

變成:
SysAdmin-Automation/
│
├── Modules/
│ └── SysAdminToolkit/
│
├── Config/
├── Scripts/
├── Tests/
├── Reports/
├── Logs/
└── State/

建立第一份 Test
例如:
Tests/
└── Get-HealthEventType.Tests.ps1

Pester 常見 Test File 名稱:
*.Tests.ps1

例如:
Get-HealthEventType.Tests.ps1
Get-ServerHealth.Tests.ps1
Write-Log.Tests.ps1

第一個 Pester Test
BeforeAll {

$ModulePath =
    Join-Path `
        $PSScriptRoot `
        "..\Modules\SysAdminToolkit\SysAdminToolkit.psd1"


Import-Module `
    $ModulePath `
    -Force `
    -ErrorAction Stop

}

Describe "Get-HealthEventType" {

It "Healthy to Critical should return NewAlert" {

    $Result =
        Get-HealthEventType `
            -PreviousStatus "Healthy" `
            -CurrentStatus "Critical"


    $Result |
    Should -Be "NewAlert"
}

}

Describe
Describe "Get-HealthEventType" {
}

可以理解為:
我要測哪一個功能?

例如:
Get-HealthEventType

It
It "Healthy to Critical should return NewAlert" {
}

代表:
我要驗證其中一種具體行為。

例如:
Healthy
↓
Critical
↓
應該 NewAlert

Should -Be
$Result |
Should -Be "NewAlert"

代表:
Result 應該等於 NewAlert。

如果:
Actual = NewAlert

Pass。
如果:
Actual = Changed

Fail。
執行 Pester
可以:
Invoke-Pester `
-Path ".\Tests"

或:
Invoke-Pester `
-Path ".\Tests\Get-HealthEventType.Tests.ps1"

如果想看更詳細:
Invoke-Pester -Path ".\Tests"
-Output Detailed

可能看到:
Describing Get-HealthEventType

[+] Healthy to Critical should return NewAlert

Tests completed
Passed: 1
Failed: 0

現在故意把 Function 改壞
假設有人修改 Module:
原本:
if (
$PreviousStatus -eq "Healthy"
) {

return "NewAlert"

}

不小心改成:
if (
$PreviousStatus -eq "Healthy"
) {

return "Changed"

}

如果沒有 Test:
Import Module
→ 沒有 Error

Script
→ 可以執行

Task Scheduler
→ 也會跑

你可能完全不知道邏輯壞了。
但:
Invoke-Pester

會直接看到:
Failed

Expected:
NewAlert

Actual:
Changed

這就是 Testing 最大的價值之一:
程式可以執行,不代表程式邏輯是正確的。

把 Day 23 的 State Transition 全測一次
可以建立:
$Cases = @(

@{
    Previous = $null
    Current  = "Healthy"
    Expected = "InitialHealthy"
}

@{
    Previous = $null
    Current  = "Critical"
    Expected = "InitialIssue"
}

@{
    Previous = "Healthy"
    Current  = "Healthy"
    Expected = "NoChange"
}

@{
    Previous = "Healthy"
    Current  = "Warning"
    Expected = "NewAlert"
}

@{
    Previous = "Healthy"
    Current  = "Critical"
    Expected = "NewAlert"
}

@{
    Previous = "Healthy"
    Current  = "Unknown"
    Expected = "VisibilityLost"
}

@{
    Previous = "Warning"
    Current  = "Warning"
    Expected = "ExistingIssue"
}

@{
    Previous = "Warning"
    Current  = "Critical"
    Expected = "Escalated"
}

@{
    Previous = "Warning"
    Current  = "Healthy"
    Expected = "Recovery"
}

@{
    Previous = "Critical"
    Current  = "Critical"
    Expected = "ExistingIssue"
}

@{
    Previous = "Critical"
    Current  = "Warning"
    Expected = "Improved"
}

@{
    Previous = "Critical"
    Current  = "Healthy"
    Expected = "Recovery"
}

@{
    Previous = "Critical"
    Current  = "Unknown"
    Expected = "VisibilityLost"
}

@{
    Previous = "Unknown"
    Current  = "Healthy"
    Expected = "Recovery"
}

@{
    Previous = "Unknown"
    Current  = "Warning"
    Expected = "StateRestoredWithIssue"
}

@{
    Previous = "Unknown"
    Current  = "Critical"
    Expected = "StateRestoredWithIssue"
}

)

Pester Parameterized Test
不用寫 16 個:
It {
}

可以:
Describe "Get-HealthEventType" {

$Cases = @(

    @{
        Previous = $null
        Current  = "Healthy"
        Expected = "InitialHealthy"
    }

    @{
        Previous = $null
        Current  = "Critical"
        Expected = "InitialIssue"
    }

    @{
        Previous = "Healthy"
        Current  = "Critical"
        Expected = "NewAlert"
    }

    @{
        Previous = "Warning"
        Current  = "Critical"
        Expected = "Escalated"
    }

    @{
        Previous = "Critical"
        Current  = "Healthy"
        Expected = "Recovery"
    }

    @{
        Previous = "Critical"
        Current  = "Warning"
        Expected = "Improved"
    }

    @{
        Previous = "Critical"
        Current  = "Unknown"
        Expected = "VisibilityLost"
    }

    @{
        Previous = "Unknown"
        Current  = "Critical"
        Expected = "StateRestoredWithIssue"
    }
)


It `
    "<Previous> -> <Current> should return <Expected>" `
    -TestCases $Cases {

    param (
        $Previous,
        $Current,
        $Expected
    )


    $Result =
        Get-HealthEventType `
            -PreviousStatus $Previous `
            -CurrentStatus $Current


    $Result |
    Should -Be $Expected
}

}

輸出就可能像:
[+] Healthy -> Critical should return NewAlert
[+] Warning -> Critical should return Escalated
[+] Critical -> Healthy should return Recovery
[+] Critical -> Warning should return Improved
[+] Critical -> Unknown should return VisibilityLost

這比手動測每個 Case 快很多。
Invalid Input 也要測
測試不只是:
正常資料

也要測:
錯誤資料

例如:
Get-HealthEventType -PreviousStatus "Healthy"
-CurrentStatus "Broken"

因為 Day 24 我們有:
[ValidateSet(
"Healthy",
"Warning",
"Critical",
"Unknown"
)]
[string]
$CurrentStatus

所以:
Broken

應該被拒絕。
Pester:
It "Should reject invalid CurrentStatus" {

{
    Get-HealthEventType `
        -PreviousStatus "Healthy" `
        -CurrentStatus "Broken"
} |
Should -Throw

}

如果哪一天有人把:
[ValidateSet()]

拿掉,
這個 Test 就可能幫我們發現:
Input Validation 被改掉了。

不只是測結果,也可以測 Module
Day 24 建立:
SysAdminToolkit.psd1

可以測:
Describe "SysAdminToolkit Module" {

It "Module manifest should be valid" {

    {
        Test-ModuleManifest `
            -Path $ModulePath `
            -ErrorAction Stop
    } |
    Should -Not -Throw
}

}

這可以避免:
Module Version 格式錯誤
RootModule 不存在
Manifest 壞掉

測試 Public Commands
例如我們期待 Module 至少提供:
Write-Log
Get-ServerHealth
Get-HealthEventType

可以:
Describe "SysAdminToolkit Public Commands" {

It "Should export Get-ServerHealth" {

    Get-Command `
        Get-ServerHealth `
        -Module SysAdminToolkit `
        -ErrorAction SilentlyContinue |
    Should -Not -BeNullOrEmpty
}


It "Should export Write-Log" {

    Get-Command `
        Write-Log `
        -Module SysAdminToolkit `
        -ErrorAction SilentlyContinue |
    Should -Not -BeNullOrEmpty
}


It "Should export Get-HealthEventType" {

    Get-Command `
        Get-HealthEventType `
        -Module SysAdminToolkit `
        -ErrorAction SilentlyContinue |
    Should -Not -BeNullOrEmpty
}

}

如果有人誤改:
Export-ModuleMember

導致:
Get-ServerHealth

突然沒有 Export,
Test 就會 Fail。
Write-Log 怎麼測?
Write-Log 會真的寫 File。
可以建立 Temporary Test File。
例如:
Describe "Write-Log" {

BeforeEach {

    $TestLog =
        Join-Path `
            $TestDrive `
            "test.log"
}


It "Should create a log entry" {

    Write-Log `
        -Path $TestLog `
        -Message "Test Message" `
        -Level "INFO"


    Test-Path $TestLog |
    Should -BeTrue
}


It "Should contain the message" {

    Write-Log `
        -Path $TestLog `
        -Message "Health Check Started"


    $Content =
        Get-Content `
            $TestLog `
            -Raw


    $Content |
    Should -Match `
        "Health Check Started"
}

}

這裡出現:
$TestDrive

Pester 的 TestDrive
Pester 可以提供一個測試用的暫存空間。
也就是:
測試開始
↓
建立 Temporary Drive
↓
寫 Test File
↓
測試結束
↓
清掉

所以不用真的:
C:\Automation\Logs

塞一堆:
test1.log
test2.log

這非常適合測:
File
JSON
CSV
Log

Save-HealthState 也非常適合測
Day 23:
Save-HealthState

負責:
Object
↓
JSON
↓
State File

可以測:
Describe "Save-HealthState" {

It "Should save health state as JSON" {

    $StateFile =
        Join-Path `
            $TestDrive `
            "state.json"


    $State = @(

        [PSCustomObject]@{
            ComputerName = "SERVER01"
            OverallStatus = "Healthy"
        }

        [PSCustomObject]@{
            ComputerName = "SERVER02"
            OverallStatus = "Critical"
        }
    )


    Save-HealthState `
        -State $State `
        -Path $StateFile


    Test-Path $StateFile |
    Should -BeTrue
}

}

再確認內容:
It "Should preserve server status" {

$SavedState =
    Get-Content `
        $StateFile `
        -Raw |
    ConvertFrom-Json


(
    $SavedState |
    Where-Object {
        $_.ComputerName -eq "SERVER02"
    }
).OverallStatus |
Should -Be "Critical"

}

哪種 Function 最容易測?
像:
Get-HealthEventType

非常容易。
因為:
Input
↓
Logic
↓
Output

它不需要:
AD
Network
Server
SMTP
Disk
WinRM

這種 Function 常被稱為比較:
Pure Logic

的 Function。
Get-ServerHealth 就比較難測
因為它依賴:
Test-WSMan
Invoke-Command
Get-CimInstance
Remote Server

如果測試每次都真的連:
SERVER01

會有很多問題。
例如:
SERVER01 今天剛好關機

Network 不通

測試環境沒有這台 Server

WinRM 被 Firewall 擋住

這時候:
Function 可能沒壞,只是環境有問題。

所以 Testing 需要開始區分不同層次。
Unit Test
主要測:
Function Logic

例如:
Healthy → Critical

NewAlert

通常希望:
快
穩定
不依賴外部環境

Integration Test
測:
PowerShell
+
Active Directory

PowerShell
+
WinRM

PowerShell
+
SMTP

PowerShell
+
File Server

例如:
Get-ServerHealth `
-ComputerName "TEST-SERVER01"

真的連到測試 Server。
這種比較接近:
Integration Test。

Smoke Test
還可以做最基本的:
Module 可以 Import 嗎?

Command 存在嗎?

Script 可以啟動嗎?

例如:
Import-Module SysAdminToolkit

成功。
再:
Get-Command `
Get-ServerHealth

存在。
這種就是很簡單但很實用的:
Smoke Test。

不要所有 Test 都連 Production
尤其:
AD Onboarding
Offboarding
Remove Group
Disable Account

更不要:
Pester 執行
↓
真的對 Production 建帳號
↓
真的 Disable User

Unit Test 應盡量:
隔離外部系統

Integration Test 也應該使用:
Lab
Test OU
Test Account
Test Server

不要把:
測試

變成:
Production Change。

Mock 的概念
如果 Function 裡:
Test-WSMan

但我們不想真的連 Server,
Pester 可以使用:
Mock

簡單理解就是:
測試時先假裝某個外部 Command 回傳特定結果。

例如原本:
Get-ServerHealth
↓
Test-WSMan SERVER01

測試時可以假裝:
Test-WSMan
永遠 Success

或者:
永遠 Throw Error

來測 Function 的 Error Handling。
為什麼 Mock 很重要?
假設我要測:
WinRM 失敗時,OverallStatus 是否會變成 Unknown?

我不需要真的去:
拔 SERVER01 網路線

只要讓測試:
假裝 Test-WSMan 失敗

就可以驗證:
Connection = Failed
OverallStatus = Unknown

不過今天先把重點放在「可測試的邏輯」
Mock 後面可以繼續深化。
Day 25 最重要的是先建立這個習慣:
Function 寫完
↓
Test Case
↓
Expected Result
↓
Invoke-Pester
↓
全部 Pass
↓
才準備部署

專案結構再增加 Tests
目前:
SysAdmin-Automation/
│
├── Modules/
│ │
│ └── SysAdminToolkit/
│ ├── SysAdminToolkit.psd1
│ ├── SysAdminToolkit.psm1
│ ├── Public/
│ └── Private/
│
├── Config/
├── Input/
│
├── Scripts/
│
├── Tests/
│ ├── Module.Tests.ps1
│ ├── Get-HealthEventType.Tests.ps1
│ ├── Write-Log.Tests.ps1
│ └── Save-HealthState.Tests.ps1
│
├── Reports/
├── Logs/
└── State/

現在已經更像一個真正軟體專案。
建立 Run-Tests.ps1
甚至可以:
Scripts/
└── Run-Tests.ps1

內容:
$TestPath =
Join-Path $PSScriptRoot
"..\Tests"

Invoke-Pester -Path $TestPath
-Output Detailed

以後修改 Module:
.\Scripts\Run-Tests.ps1

即可。
修改 Module 的流程開始改變
以前:
改 Code
↓
手動跑一下
↓
看起來正常
↓
Production

現在:
改 Code
↓
Import Module
↓
Run Pester
↓
Unit Tests
↓
全部 Pass
↓
Integration Test
↓
確認
↓
更新 Version
↓
Deploy

這就開始有:
Release Process

的概念。
測試不是證明「完全沒有 Bug」
這裡也要注意。
假設:
100 Tests Passed

不代表:
這個 Module 絕對沒有任何問題。

只能說:
我們已經驗證的 100 種條件,目前都符合預期。

所以測試的品質仍然取決於:
你測了什麼?
有沒有漏掉 Edge Case?
Expected Result 本身對不對?

Testing 是:
降低風險。

不是:
魔法保證零 Bug。

Bug 發生後,可以先加 Test
這是一個非常好用的工作方式。
假設 Production 發現:
Unknown → Warning

原本回傳
Changed

但我們希望
StateRestoredWithIssue

修 Bug 以前先寫:
It "Unknown to Warning should return StateRestoredWithIssue" {

Get-HealthEventType `
    -PreviousStatus "Unknown" `
    -CurrentStatus "Warning" |
Should -Be `
    "StateRestoredWithIssue"

}

現在 Test:
Fail

接著修改 Function。
再跑:
Pass

以後哪一天有人又改壞,
這個 Test 就會再次提醒我們。
這叫做很重要的:
Regression Test

Regression 是什麼?
簡單說:
原本已經修好的問題
↓
後來修改 Code
↓
又再次出現

例如:
v1.0.0
Recovery 正常

v1.1.0
新增 Unknown 判斷

結果
Recovery 壞了

這就是:
Regression

所以 Test 的其中一個重要任務就是:
防止舊功能被新修改弄壞。

Pester 也能幫我們做 Module Quality Gate
以後甚至可以定義:
Tests Failed
↓
不准 Deploy

例如:
Developer
↓
Modify PowerShell
↓
Git Push
↓
CI
↓
Invoke-Pester
↓
Pass?
├── Yes → Continue
└── No → Stop

你可能會發現:
這開始跟 CI/CD 很像了。

沒錯。
PowerShell Automation 其實可以接 CI
我們前面 Day 20 是:
Task Scheduler

負責:
執行維運工作。

但 Code 本身也可以有:
Git
↓
Pester
↓
CI Pipeline
↓
Deploy Module

這就是從:
SysAdmin Script

逐漸往:
Infrastructure / Platform Tooling

前進。
今天完整 Get-HealthEventType Test
整理成:
Tests/Get-HealthEventType.Tests.ps1

BeforeAll {

$ModulePath =
    Join-Path `
        $PSScriptRoot `
        "..\Modules\SysAdminToolkit\SysAdminToolkit.psd1"


Import-Module `
    $ModulePath `
    -Force `
    -ErrorAction Stop

}

Describe "Get-HealthEventType" {

Context "Initial State" {

    It "None to Healthy should return InitialHealthy" {

        Get-HealthEventType `
            -PreviousStatus $null `
            -CurrentStatus "Healthy" |
        Should -Be "InitialHealthy"
    }


    It "None to Critical should return InitialIssue" {

        Get-HealthEventType `
            -PreviousStatus $null `
            -CurrentStatus "Critical" |
        Should -Be "InitialIssue"
    }
}


Context "Healthy State" {

    It "Healthy to Healthy should return NoChange" {

        Get-HealthEventType `
            -PreviousStatus "Healthy" `
            -CurrentStatus "Healthy" |
        Should -Be "NoChange"
    }


    It "Healthy to Warning should return NewAlert" {

        Get-HealthEventType `
            -PreviousStatus "Healthy" `
            -CurrentStatus "Warning" |
        Should -Be "NewAlert"
    }


    It "Healthy to Critical should return NewAlert" {

        Get-HealthEventType `
            -PreviousStatus "Healthy" `
            -CurrentStatus "Critical" |
        Should -Be "NewAlert"
    }


    It "Healthy to Unknown should return VisibilityLost" {

        Get-HealthEventType `
            -PreviousStatus "Healthy" `
            -CurrentStatus "Unknown" |
        Should -Be "VisibilityLost"
    }
}


Context "Warning State" {

    It "Warning to Warning should return ExistingIssue" {

        Get-HealthEventType `
            -PreviousStatus "Warning" `
            -CurrentStatus "Warning" |
        Should -Be "ExistingIssue"
    }


    It "Warning to Critical should return Escalated" {

        Get-HealthEventType `
            -PreviousStatus "Warning" `
            -CurrentStatus "Critical" |
        Should -Be "Escalated"
    }


    It "Warning to Healthy should return Recovery" {

        Get-HealthEventType `
            -PreviousStatus "Warning" `
            -CurrentStatus "Healthy" |
        Should -Be "Recovery"
    }
}


Context "Critical State" {

    It "Critical to Critical should return ExistingIssue" {

        Get-HealthEventType `
            -PreviousStatus "Critical" `
            -CurrentStatus "Critical" |
        Should -Be "ExistingIssue"
    }


    It "Critical to Warning should return Improved" {

        Get-HealthEventType `
            -PreviousStatus "Critical" `
            -CurrentStatus "Warning" |
        Should -Be "Improved"
    }


    It "Critical to Healthy should return Recovery" {

        Get-HealthEventType `
            -PreviousStatus "Critical" `
            -CurrentStatus "Healthy" |
        Should -Be "Recovery"
    }


    It "Critical to Unknown should return VisibilityLost" {

        Get-HealthEventType `
            -PreviousStatus "Critical" `
            -CurrentStatus "Unknown" |
        Should -Be "VisibilityLost"
    }
}


Context "Unknown State" {

    It "Unknown to Healthy should return Recovery" {

        Get-HealthEventType `
            -PreviousStatus "Unknown" `
            -CurrentStatus "Healthy" |
        Should -Be "Recovery"
    }


    It "Unknown to Warning should return StateRestoredWithIssue" {

        Get-HealthEventType `
            -PreviousStatus "Unknown" `
            -CurrentStatus "Warning" |
        Should -Be "StateRestoredWithIssue"
    }


    It "Unknown to Critical should return StateRestoredWithIssue" {

        Get-HealthEventType `
            -PreviousStatus "Unknown" `
            -CurrentStatus "Critical" |
        Should -Be "StateRestoredWithIssue"
    }
}


Context "Input Validation" {

    It "Invalid CurrentStatus should throw" {

        {

            Get-HealthEventType `
                -PreviousStatus "Healthy" `
                -CurrentStatus "Broken"

        } |
        Should -Throw
    }
}

}

Context 是什麼?
你會看到:
Context "Critical State" {
}

它主要是幫測試:
分類。

例如:
Describe
Get-HealthEventType

├── Context Initial State
├── Context Healthy State
├── Context Warning State
├── Context Critical State
├── Context Unknown State
└── Context Input Validation

之後 Test 越多,會比全部塞在同一層更容易閱讀。
我們現在可以開始建立「改 Code 前先跑 Test」習慣
例如今天準備改:
Get-HealthEventType

第一步先:
Invoke-Pester -Path ".\Tests"
-Output Detailed

先確認:
Before Change
All Passed

再修改。
修改完:
Invoke-Pester

如果:
20 Passed
0 Failed

至少知道:
現有測試涵蓋的行為沒有 Regression。

Day 25 小結
今天沒有新增:
Server Health
AD Function
HTML Report
Email Alert

而是開始替我們前 24 天累積的 Automation 加一層非常重要的保護:
Automated Testing
一開始我們從:
Expected
vs
Actual

開始。
接著使用:
Describe
Context
It
Should
Invoke-Pester

建立真正的 PowerShell Tests。
今天最適合拿來測的就是:
Get-HealthEventType

因為它幾乎完全是:
Input
↓
Logic
↓
Output

不需要真的:
連 Server
連 AD
連 SMTP

所以非常適合做:
Unit Test

同時我們也開始區分:
Unit Test
→ Function Logic

Integration Test
→ 真正外部系統

Smoke Test
→ 最基本的 Module / Command 是否正常

並開始理解:
Mock

可以讓我們未來不必真的把 Server 關機,就可以測:
WinRM Failure
Connection Failure

這些 Error Handling。
今天另一個重要觀念是:
Automation 可以跑完,不代表 Automation 是正確的。

以前:
PowerShell 沒跳 Error

我以為成功

現在:
Function Output
↓
Expected Result
↓
Automated Test
↓
Pass / Fail

開始用比較工程化的方式證明程式行為。
到目前為止,我們的流程已經變成:
PowerShell Code
↓
Module
↓
Pester Tests
↓
Pass
↓
Deploy
↓
Task Scheduler
↓
Automation

而不是:
改完
↓
直接 Production

這也是從:
「會寫 PowerShell」

往:
「會維護 PowerShell Automation」

非常重要的一步。
Day 26 預告
Day 26|PowerShell 與 Git:讓每一次 Script 修改都有版本紀錄
今天我們解決:
怎麼知道改完有沒有壞?

但是還有另一個問題。
假設昨天:
SysAdminToolkit 1.1.0
正常

今天修改:
Get-ServerHealth

結果 Production 出問題。
這時候有人問:
昨天正常的版本到底是哪一份?

如果資料夾長這樣:
SysAdminToolkit.psm1

SysAdminToolkit_old.psm1

SysAdminToolkit_new.psm1

SysAdminToolkit_new2.psm1

SysAdminToolkit_final.psm1

SysAdminToolkit_final_真的最後.psm1

其實已經失控了。
所以 Day 26 會正式把整個專案放進:
Git

開始使用:
git init
git status
git add
git commit
git log
git diff

讓我們可以回答:
誰改了?
改了什麼?
什麼時候改?
上一版是什麼?
哪個版本開始出問題?

也會建立 .gitignore,避免把:
Logs/
Reports/
State/
Temporary Files
Secrets

這些不應該進版本控制的資料一起 Commit。
Day 26 開始,我們的 SysAdmin Automation Toolkit 就不只是一堆 PowerShell 檔案,而會真正進入:
Version Control + Testing + Automation

的工程化流程。


上一篇
Day 24|建立自己的 PowerShell Module:把散落的 Function 整理成真正的維運工具箱
下一篇
Day 26|PowerShell 與 Git:讓每一次 Script 修改都有版本紀錄
系列文
系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言