Pester로 하는 PowerShell 테스트 정비 ── 운영 스크립트를 쉽게 망가지지 않게 만드는 실무 패턴
· 小村 豪 · PowerShell, Pester, Windows, 테스트, 자동화, CI, 기존 자산 활용
1. 가장 먼저 챙겨야 할 것
PowerShell 스크립트는 처음에는 작은 작업을 자동화하는 정도로 시작됩니다.
파일을 모은다. 로그를 검색한다. CSV를 만든다. 오래된 파일을 이동한다. 서비스 상태를 확인한다.
어느 것이든 수십 줄 정도라면 눈으로 보고 동작을 확인할 수 있습니다. 하지만 실무에서 계속 사용하다 보면, 이런 변경이 들어옵니다.
- 대상 폴더를 늘린다
- 제외 조건을 추가한다
- CSV의 열을 바꾼다
- 삭제 전에 아카이브한다
- 작업 스케줄러나 CI에서 실행한다
- 오류 발생 시 알림을 보낸다
이 단계가 되면, 「한 번 로컬에서 동작했다」는 것만으로는 부족합니다.
PowerShell의 무서움은 편리함과 한 몸입니다. 읽기만 하는 처리라면 가볍게 시도해볼 수 있지만, 삭제, 이동, 덮어쓰기, 서비스 재시작, 권한 변경 같은 처리는 조건을 조금만 잘못 지정해도 사고로 이어집니다.
그래서 사용하고 싶은 것이 PowerShell용 테스트 프레임워크인 Pester입니다. 이 글에서는 Pester의 기능을 전부 다루는 대신, 이미 존재하는 PowerShell 스크립트를 실무에서 쉽게 망가지지 않도록 만드는 「테스트 정비」의 진행 방식을 정리합니다.
PowerShell의 테스트는 단순히 깨끗한 코드를 작성하기 위한 것만이 아닙니다. 변경 전의 불안을 줄이고, 변경 후에 근거를 가지고 확인하기 위한 도구입니다.
또한, 이 글에 등장하는 코드는 Invoke-Pester로 실행할 수 있는 샘플 세트(테스트 대상 스크립트, Pester 테스트, CI용 실행 스크립트)로 GitHub에 공개되어 있습니다.
pester-powershell-test-maintenance - komurasoft-blog-samples (GitHub)
2. Pester로 무엇을 지키는가
Pester를 도입한다고 모든 것이 자동으로 안전해지는 것은 아닙니다. 먼저 정해야 할 것은 「무엇을 테스트로 지킬 것인가」입니다.
PowerShell 운영 스크립트에서는 다음 4가지를 우선하면 효과를 보기 쉽습니다.
| 지키고 싶은 것 | 테스트에서 보는 내용 |
|---|---|
| 조건 판정 | 어떤 파일, 행, 사용자, 서비스를 대상으로 할 것인가 |
| 출력 형태 | 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 버전이 서로 다르지 않은지 확인합니다.
PowerShell 테스트에서 흔히 발생하는 혼란은, 코드의 문제가 아니라 테스트 러너의 버전 차이에서 비롯됩니다.
특히 오래된 글이나 사내 메모에는 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를 직접 사용하면, 테스트를 실행하는 날짜에 따라 결과가 달라집니다. 날짜를 인수로 두면 「2026년 6월 1일 기준으로 30일보다 오래된 파일」이라는 조건을 고정해서 테스트할 수 있습니다.
다음으로, 테스트를 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. 테스트 이름은 사양으로 작성한다
Pester의 It에 작성하는 문자열은 단순한 설명이 아니라, 나중에 읽는 사람에게는 작은 사양서가 됩니다.
예를 들어, 다음과 같은 이름은 조금 약합니다.
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
짧고 편리하지만 테스트하기 어렵습니다. 대상 선정과 삭제가 하나의 파이프라인으로 이어져 있어서, 어디를 확인해야 할지 알기 어렵습니다.
실무에서는 다음과 같이 분리합니다.
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 함수에서는, 가능한 한 -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 'WhatIf에서는 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') `
-WhatIf
Should -Invoke Remove-Item -Times 0
}
}
이 테스트에서는 Get-OldLogFile도 Remove-Item도 Mock으로 처리하고 있으므로, 실제 C:\Logs\old.log가 존재하지 않아도 상관없습니다. 보고 있는 것은 Remove-OldLogFile의 판단입니다.
- 대상이 있으면
Remove-Item을 호출한다 -WhatIf일 때는Remove-Item을 호출하지 않는다- 호출할 때는 의도한 경로를 넘긴다
위험한 처리일수록, 실행 자체가 아니라 호출 조건을 테스트하는 쪽이 안전합니다.
11. Mock을 지나치게 사용하지 않는다
Mock은 편리하지만, 지나치게 사용하면 테스트의 가치가 떨어집니다. 모든 것을 Mock으로 처리하면 실제 PowerShell의 동작과 지나치게 멀어지기 때문입니다.
기준은 다음과 같습니다.
| 처리 | 추천 방식 |
|---|---|
| 날짜 | 인수로 고정한다 |
| 파일 생성 | $TestDrive를 사용한다 |
| 삭제·이동 | Mock과 -WhatIf로 확인한다 |
| Web API 호출 | Invoke-RestMethod 등을 Mock한다 |
| 메일 발송·알림 | 발송 명령을 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에서는 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
현장에서는 모든 테스트를 매번 실행하려고 하면 지속하기 어려운 경우가 있습니다.
먼저 빠르고 부작용이 없는 테스트를 기본으로 삼고, 환경에 의존하는 테스트는 태그로 나누어 필요한 시점에 실행합니다.
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용 출력 형식, 커버리지, 종료 코드 등은 실행용 스크립트에 모아두면 정리하기 쉬워집니다.
15. 커버리지는 목표가 아니라 지도로 본다
Pester에서는 코드 커버리지도 낼 수 있습니다. 다만 처음부터 숫자를 지나치게 좇지 않는 쪽이 좋습니다. 커버리지는 테스트의 품질 자체가 아니기 때문입니다.
예를 들어, 삭제 대상 추출 함수를 한 번 호출하면 그 행은 통과한 것이 됩니다. 하지만 경계 조건이나 제외 조건이 확인되지 않았다면, 실무상의 안심으로는 이어지지 않습니다.
커버리지는 다음과 같이 사용합니다.
- 전혀 통과하지 않은 함수를 찾는다
- 중요한데도 테스트가 없는 분기를 찾는다
- 변경 빈도가 높은 스크립트부터 우선순위를 정한다
- CI에서 테스트 실행의 증적을 남긴다
숫자를 올리는 것보다, 「중요한 판단이 테스트되고 있는가」를 봅니다.
16. 기존 스크립트에 대한 도입 순서
기존 PowerShell 자산에 Pester를 도입할 경우, 처음부터 전체를 테스트 대상으로 삼지 않는 쪽이 진행하기 쉽습니다. 추천하는 순서를 소개합니다.
1. 실패하면 곤란한 스크립트를 고른다
먼저 이런 스크립트가 적합합니다.
- 삭제, 이동, 덮어쓰기를 포함한다
- 매일 또는 매월 실행된다
- 매뉴얼에는 남아 있지만 특정 담당자에게 의존하고 있다
- 과거에 조건 실수가 있었다
- 출력 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한다 - 날짜는 고정할 수 있도록 인수화한다
Describe또는It에Unit,Smoke,RequiresAdmin같은 태그를 붙인다- CI에서는
Unit을 기본으로 실행한다 - 변경계 함수에는 가능한 한
SupportsShouldProcess를 붙인다 - 과거의 문제는 재발 방지 테스트로 남긴다
규칙이 너무 많으면 지켜지지 않습니다. 처음에는 이 3가지만으로도 충분합니다.
TestDrive를 사용한다
날짜를 고정한다
위험한 작업은 Mock한다
이 3가지만 지켜도, PowerShell 테스트는 상당히 안정됩니다.
19. 어디까지 테스트하지 않을지도 정한다
테스트 정비에서는 「무엇을 테스트할 것인가」만큼 「무엇을 테스트하지 않을 것인가」도 중요합니다.
예를 들어, 다음과 같은 부분은 단위 테스트로 무리하게 확인하지 않는 쪽이 좋습니다.
- Windows 자체의
Get-ChildItem이 올바르게 동작하는 것 Remove-Item이 실제로 파일을 삭제하는 것- PowerShell 표준 명령의 내부 사양
- 외부 API가 항상 응답하는 것
- 네트워크 공유가 항상 사용 가능한 것
테스트해야 하는 것은 우리 자신의 판단입니다.
- 어떤 조건에서 대상으로 삼는가
- 어떤 경로를 넘기는가
- 어떤 열을 출력하는가
- 실패 시 어떻게 처리하는가
- 위험한 작업을 예행 연습할 수 있는가
표준 명령을 믿는 부분과, 우리 자신의 로직을 지키는 부분을 나눕니다.
20. 정리
PowerShell은 일상 업무를 빠르게 자동화할 수 있는 편리한 도구입니다. 하지만 실무에서 오래 사용하는 스크립트는 조금씩 책임이 무거워집니다. 처음에는 자신만 사용하는 한 줄짜리 명령이었던 것이, 어느새 매일 실행되는 운영 처리가 되고, 다른 사람의 작업이나 업무 데이터에 영향을 주게 됩니다.
Pester를 이용한 테스트 정비는, 이러한 변화에 맞춰 스크립트를 지키기 위한 작업입니다.
포인트를 정리해 둡니다.
- 먼저 대상 선정을 테스트한다
- 날짜와 경로를 외부에서 넘길 수 있게 한다
- 파일 작업은
$TestDrive에 가둔다 - 삭제, 이동, 알림, API 호출은
Mock한다 - 변경계 함수는
-WhatIf로 예행 연습할 수 있게 한다 - 테스트 이름을 사양으로 읽을 수 있게 한다
- CI에서는 빠르고 부작용이 없는 테스트부터 실행한다
PowerShell의 안전한 운영은 갑자기 거대한 체계를 도입하는 것이 아닙니다.
작은 함수로 나눈다. 작은 테스트를 작성한다. 위험한 처리 앞에서 확인할 수 있는 형태로 만든다.
이런 축적을 통해, PowerShell은 「편리하지만 조금 무서운 스크립트」에서, 「변경해도 확인할 수 있는 업무 도구」에 가까워집니다.
참고 링크
- 이 글의 샘플 코드 전체(테스트 대상 스크립트, Pester 테스트, CI용 실행 스크립트) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/pester-powershell-test-maintenance
- Pester Quick Start
- Pester Installation and Update
- Pester File placement and naming
- Pester TestDrive
- Pester Mocking
- Pester Tags
- Pester Configuration
- Pester Test Results
- Pester Code Coverage
- PowerShell Gallery: Pester
- PowerShell Documentation - Microsoft Learn
관련 글
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
C#(CSharp)에서 PowerShell을 실행하고 결과를 객체로 받는 방법
C#에서 PowerShell을 실행하고 결과를 문자열이 아닌 PSObject로 받는 방법을, PowerShell SDK, AddCommand, AddParameter, BaseObject, Properties, 오류 처리까지 실무 관점에서 정리합니다.
PowerShell 실용 명령어 모음 ── 일상 업무에서 자주 쓰는 작은 기능 늘리기
PowerShell로 일상 업무에 쓰는 실용 명령어로서, Measure-Object, Group-Object, Select-String, Compare-Object, Tee-Object, Start-Transcript 등의 활용 시점을 정리합니다.
PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화하기
PowerShell 스크립트로 로그 조사, CSV 리포트, 오래된 로그 아카이브, 증적 보존, 작업 스케줄러 실행까지 안전하게 진행하는 실무 절차를 정리합니다.
PowerShell 명령어의 기초 — 가장 먼저 익힐 조작과 안전한 사용법
PowerShell 초보자가 실무에서 헤매지 않도록, 명령어(cmdlet)를 찾는 방법부터 파이프라인, 파일 조작, CSV 처리, 실행 정책, 사고를 막기 위한 확인 절차까지 정리합니다.
VBScript 폐지에 대비하는 VBA·사내 도구 점검 가이드
VBScript 단계적 폐지에 대비해 VBA·Excel 매크로·사내 도구의 재고 조사, 정적 검출, 실행 로그, 대체 기술 선정, 테스트, 단계적 전개를 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Pester로 가장 먼저 무엇을 테스트해야 하나요?
- 삭제 처리 자체가 아니라, 삭제 대상을 선정하는 처리(대상 선정)부터 시작합니다. 운영 스크립트에서는 「조건 판정」「출력 형태」「위험한 작업 직전」「외부 의존」이라는 4가지를 우선시하면 효과를 보기 쉽습니다. 기존 스크립트를 처음부터 크게 뜯어고칠 필요는 없으며, 위험한 처리 앞에 있는 판단 부분을 함수로 분리하고, 그 반환값을 Pester로 확인하는 것부터 시작합니다.
- Pester의 TestDrive란 무엇인가요?
- $TestDrive는 Pester가 제공하는 테스트용 임시 영역입니다. 실제 C:\Logs나 공유 폴더를 사용하지 않고, 테스트 안에서만 만든 파일로 파일 작업을 검증할 수 있습니다. 파일 작업을 포함하는 PowerShell 스크립트 테스트에서는, 먼저 $TestDrive를 사용하는 습관을 들이면 실제 파일을 잘못 지우는 사고를 방지할 수 있습니다.
- Pester의 Mock은 어디까지 사용해야 하나요?
- 삭제·이동, Web API 호출, 메일 발송·알림처럼 실행해서는 안 되는 처리는 Mock으로 대체하고, 날짜는 인수로 고정하며, 파일 생성이나 CSV 읽기/쓰기는 $TestDrive에 작은 실제 파일을 만드는 쪽이 적합합니다. 모든 것을 Mock으로 처리하면 실제 PowerShell의 동작과 지나치게 멀어져서 문자 코드, 줄바꿈, 열 이름 문제를 놓칠 수 있으므로, 「실제로 사용하는 부분」과 「Mock으로 처리하는 부분」을 나누는 것이 핵심입니다.
- 로컬에서는 통과하는 Pester 테스트가 CI에서는 실패하는 이유는 무엇인가요?
- 흔한 원인은 현재 디렉터리의 차이로, 테스트 대상을 $PSScriptRoot 기준으로 불러오면 해결할 수 있습니다. 날짜에 따라 결과가 달라진다면 Get-Date를 직접 사용하는 것이 원인이므로, -Now와 같은 인수로 날짜를 고정합니다. Mock이 동작하지 않는다면 모듈 경계나 스코프 차이를 의심하고, -ModuleName이나 로드 방식을 확인합니다. Pester의 문제처럼 보이지만, 실제로는 스크립트 구조가 원인인 경우도 많습니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크