수정 이력(3건, 최종 수정 2026년 08월 02일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- 글 맨 앞에 "이 글의 지식 맵" 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 모은 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대한 대응으로 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참고하십시오.
- GitHub Actions와 Azure Pipelines의 완전한 설정 예를 추가했습니다. 더불어 사내 환경에서 Install-Module이 막히는 원인과 조치, 스크립트 종류별로 단위 테스트에서 어디를 볼지의 대응표, 커버리지 설정 예를 추가했습니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635296)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
小村 豪 (2026). 「Pester로 PowerShell 테스트를 갖추기 ── 운영 스크립트를 쉽게 깨지지 않게 만드는 실무 패턴」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635296 https://comcomponent.com/ko/blog/pester-powershell-test-maintenance/
- DOI(최신 버전)
- 10.5281/zenodo.21635296
- DOI(이 버전)
- 10.5281/zenodo.21635297
1. 가장 먼저 잡아 둘 것
PowerShell 스크립트는 처음에는 작은 작업의 자동화로 시작됩니다.
파일을 모읍니다. 로그를 검색합니다. CSV를 만듭니다. 오래된 파일을 이동합니다. 서비스 상태를 확인합니다.
어느 것이든 수십 줄이면 눈으로 보고 동작을 확인할 수 있습니다. 다만 실무에서 계속 쓰다 보면 이런 변경이 들어옵니다.
- 대상 폴더를 늘린다
- 제외 조건을 추가한다
- CSV의 열을 바꾼다
- 삭제 전에 아카이브한다
- 작업 스케줄러나 CI에서 실행한다
- 오류 시 알림을 보낸다
이 단계가 되면 “한 번 로컬에서 동작했다”만으로는 부족합니다.
PowerShell의 위험은 편리함과 맞닿아 있습니다. 읽기만 하는 처리라면 가볍게 시험해 볼 수 있지만, 삭제, 이동, 덮어쓰기, 서비스 재시작, 권한 변경 같은 처리는 조건을 조금만 잘못 잡아도 사고로 이어집니다.
그래서 필요한 것이 PowerShell용 테스트 프레임워크인 Pester입니다. 이 글에서는 Pester 기능을 빠짐없이 다루지 않습니다. 이미 있는 PowerShell 스크립트를 실무에서 쉽게 깨지지 않게 만들기 위한 “테스트 기반을 갖추는” 진행 방식을 정리합니다.
PowerShell 테스트는 깨끗한 코드를 쓰기 위한 것만은 아닙니다. 변경 전에 불안을 줄이고, 변경 후에 근거를 가지고 확인하기 위한 도구입니다.
참고로, 이 글에 나오는 코드는 Invoke-Pester로 실행할 수 있는 샘플 세트(테스트 대상 스크립트, Pester 테스트, CI용 실행 스크립트)로 GitHub에 공개되어 있습니다.
pester-powershell-test-maintenance - komurasoft-blog-samples (GitHub)
이 글의 지식 맵
이 글은 PowerShell 운영 스크립트를 Pester v5로 쉽게 깨지지 않게 만드는 실무 절차를, 테스트 대상을 지키는 구조의 관계로 정리합니다. Pester는 $TestDrive로 실제 파일에 손대지 않고 파일 작업을 검증하고, Mock으로 삭제나 알림 같은 위험 작업을 실행하지 않은 채 호출 조건만 확인합니다. 삭제 대상을 고르는 함수를 위험한 변경 처리보다 먼저 분리해 테스트하고, 일시는 Get-Date를 직접 호출하지 않고 인수로 넘겨 테스트의 불안정성을 막습니다. GitHub Actions와 Azure Pipelines는 Pester 실행을 자동화하지만, Windows에 동봉된 구버전 Pester와는 별도로 명시적인 재설치가 필요하며, 사내 프록시나 TLS 설정이 Install-Module을 통한 모듈 도입을 가로막기도 합니다.
flowchart LR
accTitle: Pester를 통한 PowerShell 테스트 정비의 지식 맵
accDescr: Pester가 PowerShell 스크립트를 검증하는 테스트 프레임워크이며, TestDrive로 파일 작업을, Mock으로 삭제 등 위험 작업을 안전하게 검증하고, 대상 선정 함수를 위험 작업보다 먼저 굳히며, 일시를 인수로 넘겨 테스트 불안정성을 막고, GitHub Actions나 Azure Pipelines로 CI를 자동화하는 한편 Windows에 동봉된 구버전과의 차이나 사내 프록시로 인한 도입 저해에 주의해야 함을 보여주는 그림입니다.
pester["Pester"]
powershell["PowerShell"]
pester_testdrive["$TestDrive"]
pester_mock["Pester Mock"]
pester_tag["Pester Tag"]
pester_code_coverage["Pester Code Coverage"]
supportsshouldprocess["SupportsShouldProcess(-WhatIf)"]
destructive_operation_powershell["위험 조작(삭제·이동·덮어쓰기 등)"]
github_actions["GitHub Actions"]
azure_pipelines["Azure Pipelines"]
corporate_proxy_module_install_blocker["사내 프록시/TLS에 의한 모듈 도입 저해"]
install_module_powershellgallery["Install-Module로 PowerShell Gallery에서 도입"]
closed_network["폐쇄망"]
target_selection_function["대상 선정을 분리한 함수"]
date_argument_injection["날짜 인수화(Now 인수)"]
test_flakiness["테스트 불안정성(flaky test)"]
pester -->|"이용한다"| powershell
pester -->|"이용한다"| pester_testdrive
pester -->|"이용한다"| pester_mock
pester -->|"이용한다"| pester_tag
pester -->|"이용한다"| pester_code_coverage
supportsshouldprocess -->|"완화한다"| destructive_operation_powershell
pester_mock -->|"권장되는 대응"| destructive_operation_powershell
pester_testdrive -->|"완화한다"| destructive_operation_powershell
github_actions -->|"자동화한다"| pester
azure_pipelines -->|"자동화한다"| pester
corporate_proxy_module_install_blocker -.->|"양립하지 않는다"| install_module_powershellgallery
closed_network -->|"양립하지 않는다"| install_module_powershellgallery
destructive_operation_powershell -->|"에서 확인할 수 있다"| pester_mock
github_actions -.->|"이용한다"| pester_tag
install_module_powershellgallery -->|"전제로 한다"| powershell
target_selection_function -->|"보다 먼저 해야 한다"| destructive_operation_powershell
date_argument_injection -.->|"방지한다"| test_flakiness
target_selection_function -.->|"이용한다"| date_argument_injection
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 18건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
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
-SkipPublisherCheck를 붙인 이유는, Windows에 함께 들어 있는 오래된 Pester가 Microsoft 서명으로 설치되어 있기 때문입니다. 이 옵션이 없으면 게시자가 다르다는 이유로 설치가 멈춥니다.
사내 PC에서는 이 명령이 그대로는 통과하지 않는 경우가 있습니다. 프록시, TLS 1.2, NuGet 공급자가 전형적인 원인입니다. 조치는 17장에 모았습니다.
팀에서 쓸 때는 로컬 개발 PC, 빌드 서버, 작업 실행 환경에서 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
현장에서는 모든 테스트를 매번 실행하려고 하면 이어지지 않는 경우가 있습니다.
우선 빠르고 부작용이 없는 테스트를 표준으로 두고, 환경에 의존하는 테스트는 태그로 나눠 필요한 시점에 실행합니다.
스크립트 종류별 대응표
이 스크립트는 어디까지가 단위 테스트이고 어디부터가 통합 테스트인지, 그때 무엇을 쓸지는 스크립트 종류로 대략 정해집니다.
| 스크립트 종류 | 단위 테스트에서 보는 곳 | 쓰는 방법 | 통합 테스트에서 보는 곳 |
|---|---|---|---|
| 파일 수집·대상 선정 | 조건의 경계. 며칠 전까지 포함하는지, 확장자로 걸러지는지 | $TestDrive에 작은 실제 파일을 만든다 |
실제 폴더에서 건수가 예상과 맞는지 |
| CSV·JSON 입출력 | 열 이름, 열 순서, 문자 인코딩, 줄바꿈 | $TestDrive에 실제 파일을 읽고 쓴다 |
넘겨받는 시스템에서 실제로 열리는지 |
| 삭제·이동·덮어쓰기 | 삭제 대상 선정 결과와 -WhatIf 예행 |
Mock Remove-Item / Mock Move-Item |
검증 환경에서 한 번만 실행해 결과를 확인 |
| 서비스·프로세스 조작 | 상태에서 다음 조작을 정하는 판단 로직 | Mock Get-Service 등으로 상태를 만든다 |
검증기에서 실제로 시작·중지한다 |
| 외부 API·알림·메일 발송 | 조립한 요청이나 본문 내용 | Mock Invoke-RestMethod / Mock Send-MailMessage |
스테이징 대상으로 한 번만 보낸다 |
| 날짜·기간으로 판단하는 처리 | 경계일 판정. 당일, 전날, 윤일 | 날짜를 인수로 고정한다(Mock하지 않는다) |
― |
| 레지스트리·시스템 설정 읽기 | 읽은 값을 어떻게 해석할지 | Mock Get-ItemProperty |
실제 기기에서 실제 값을 확인한다 |
보는 관점은 두 가지입니다.
- 단위 테스트는 우리 쪽 판단을 본다. 표준 명령이 올바르게 동작하는지는 확인하지 않습니다(19장).
- 위험한 작업은 단위 테스트에서 실행하지 않는다. 삭제, 이동, 발송, 알림은
Mock으로 바꾸고, 실행 자체는 통합 테스트에서 횟수를 줄여 확인합니다.
날짜만은 Mock이 아니라 인수로 고정한다는 점에 주의하세요. Get-Date를 Mock하면 같은 스크립트 안의 다른 용도 날짜 조회까지 함께 건드립니다.
14. CI에서 실행한다
Pester는 로컬에서만 실행해도 도움이 됩니다. 다만 팀에서 스크립트를 관리한다면 CI에서 실행할 수 있게 해 두는 편이 안심입니다.
예를 들어 tools/Invoke-ProjectTests.ps1 같은 파일을 준비합니다.
$ErrorActionPreference = 'Stop'
# 환경에 오래된 Pester가 남아 있을 수 있으므로 v5 이상을 명시해 불러온다
Import-Module Pester -MinimumVersion 5.0.0
$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용 출력 형식, 커버리지, 종료 코드 등은 실행용 스크립트에 모으면 정리하기 쉽습니다.
이 실행용 스크립트가 있으면 CI 쪽 설정은 어느 서비스든 짧게 끝납니다.
GitHub Actions인 경우
.github/workflows/pester.yml을 둡니다.
name: pester
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Pester v5
shell: pwsh
run: |
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck
- name: Run Pester
shell: pwsh
run: ./tools/Invoke-ProjectTests.ps1
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: pester-results
path: |
test-results.xml
coverage.xml
잡아 둘 점은 다음입니다.
- Pester는 명시적으로 넣는다. Windows에는 Pester v3 계열이 함께 들어 있어, 다시 넣지 않으면 v5 작성법이 동작하지 않습니다.
-SkipPublisherCheck를 붙인다. 함께 들어 있는 Pester에는 Microsoft 서명이 있으므로, 이 옵션이 없으면 게시자가 다르다는 이유로 설치가 멈춥니다.shell: pwsh를 명시한다. GitHub-hosted Windows 러너는 기본이pwsh이지만, self-hosted 러너에서는 PowerShell 7이 없으면 Windows PowerShell로 떨어집니다. 명시해 두면 환경 차로 고민하지 않아도 됩니다.if: always()로 결과 파일을 회수한다. 테스트가 실패했을 때야말로 결과를 보고 싶으므로, 실패 시에도 실행되게 합니다.
잡을 실패시키는 것은 $config.Run.Exit = $true입니다. 이것이 있으면 테스트가 실패했을 때 Invoke-Pester가 0이 아닌 종료 코드로 끝나고, 스텝도 실패합니다. 반대로 이것을 빼먹으면 테스트가 빨개도 잡은 초록이 됩니다.
Azure Pipelines인 경우
azure-pipelines.yml은 다음과 같습니다.
trigger:
- main
pool:
vmImage: 'windows-latest'
steps:
- task: PowerShell@2
displayName: 'Install Pester v5'
inputs:
pwsh: true
targetType: 'inline'
script: |
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck
- task: PowerShell@2
displayName: 'Run Pester'
inputs:
pwsh: true
filePath: 'tools/Invoke-ProjectTests.ps1'
- task: PublishTestResults@2
displayName: 'Publish test results'
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results.xml'
failTaskOnFailedTests: true
PublishTestResults@2가 받는 형식은 JUnit, NUnit, VSTest, XUnit, CTest입니다. 실행용 스크립트에서 $config.TestResult.OutputFormat = 'JUnitXml'로 두었으므로 여기서는 JUnit을 고릅니다. 출력 형식을 바꿀 때는 양쪽을 맞춰 바꾸세요.
failTaskOnFailedTests: true를 넣어 두면 결과 파일에 실패가 들어 있는 시점에 태스크가 실패합니다. 기본값은 false이며, 그 경우에는 결과를 표시하기만 하고 통과해 버립니다.
15. 커버리지는 목표가 아니라 지도로 본다
Pester에서는 코드 커버리지도 낼 수 있습니다. 다만 처음부터 숫자를 너무 쫓지 않는 편이 낫습니다. 커버리지는 테스트 품질 그 자체가 아니기 때문입니다.
예를 들어 삭제 대상 추출 함수를 한 번 호출하면 그 줄은 통과한 것이 됩니다. 그러나 경계 조건이나 제외 조건이 확인되지 않았다면 실무상의 안심으로는 이어지지 않습니다.
커버리지는 이렇게 씁니다.
- 전혀 통과하지 않은 함수를 찾는다
- 중요한데 테스트가 없는 분기를 찾는다
- 변경 빈도가 높은 스크립트부터 우선순위를 매긴다
- CI에서 테스트 실행의 증적을 남긴다
숫자를 올리는 것보다 “중요한 판단이 테스트되고 있는지”를 봅니다.
취득은 New-PesterConfiguration의 CodeCoverage 쪽에서 설정합니다.
$config = New-PesterConfiguration
$config.Run.Path = @('.\tests')
$config.CodeCoverage.Enabled = $true
# 측정 대상. 테스트 파일이 아니라 테스트 대상 스크립트를 지정한다
$config.CodeCoverage.Path = @('.\src')
# 출력 형식. 기본값은 JaCoCo이며 CI의 커버리지 표시에 넣기 쉽다
$config.CodeCoverage.OutputFormat = 'JaCoCo'
$config.CodeCoverage.OutputPath = '.\coverage.xml'
# 목표값. 미달해도 테스트는 실패하지 않으므로 어디까지나 기준으로 둔다
$config.CodeCoverage.CoveragePercentTarget = 75
Invoke-Pester -Configuration $config
지정하는 CodeCoverage.Path는 테스트 파일이 아니라 테스트 대상 스크립트입니다. 여기를 .\tests로 두면 테스트 코드 자체의 커버리지를 재게 되어, 숫자는 높은데 알맹이가 없는 상태가 됩니다.
출력 형식의 기본값은 JaCoCo입니다. CoverageGutters를 고르면 에디터에서 줄마다 통과 여부를 볼 수 있는 형식이 됩니다. CI에 넣을 거라면 JaCoCo 그대로여도 문제 없습니다.
CoveragePercentTarget은 기준 숫자일 뿐, 미달해도 실행이 실패하는 것은 아닙니다. 커버리지를 합격 조건으로 두고 싶어질 때야말로 이 장의 처음으로 돌아가세요. 숫자를 채우기 위한 테스트는 대개 깨졌을 때 아무것도 알려 주지 않습니다.
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 문제처럼 보여도, 실제로는 스크립트 구조가 원인인 경우가 많습니다.
테스트하기 어려운 부분은 대개 운영에서도 깨지기 쉬운 부분입니다.
Install-Module에서 막힌다(사내 환경에 많다)
3장의 Install-Module은 인터넷에 직접 나갈 수 있는 환경이면 그대로 통과합니다. 반면 IT 부서가 관리하는 사내 PC에서는 여기서 멈추는 일이 많습니다.
| 증상 | 원인 | 대응 |
|---|---|---|
| PowerShell Gallery에 연결할 수 없다 | 사내 프록시를 거치지 않는다 | -Proxy와 -ProxyCredential을 지정한다 |
| “기본 연결이 닫혔습니다” 등으로 실패한다 | Windows PowerShell 5.1 기본 프로토콜에 TLS 1.2가 들어 있지 않다 | 세션에서 TLS 1.2를 켠 뒤 실행한다 |
| NuGet 공급자 도입을 물어 멈춘다 | Windows PowerShell에 함께 들어 있는 PowerShellGet 1.0.0.1 그대로다 | 먼저 NuGet 공급자를 넣는다 |
| 애초에 외부로 나갈 수 없다 | 폐쇄 네트워크 | 다른 PC에서 Save-Module한 뒤 가져오거나, 사내 리포지토리를 Register-PSRepository로 등록한다 |
위 세 가지는 이 순서로 실행하면 통과하는 경우가 많습니다.
# 1. TLS 1.2를 켠다(Windows PowerShell 5.1에서 필요. PowerShell 7에서는 불필요)
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor
[Net.SecurityProtocolType]::Tls12
# 2. NuGet 공급자를 넣는다(대화형 프롬프트에서 멈추지 않도록 먼저 넣는다)
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force
# 3. 프록시를 거쳐 Pester를 넣는다
$proxyUri = 'http://proxy.example.local:8080'
$proxyCredential = Get-Credential -Message '프록시 인증'
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck `
-Proxy $proxyUri -ProxyCredential $proxyCredential
TLS 1.2 설정은 그 세션에서만 먹습니다. 매번 입력하기 번거로우면 프로필 스크립트에 적어 둡니다.
프록시에 인증이 필요 없으면 -ProxyCredential은 생략할 수 있습니다. 프록시 URL을 모르면 IT 부서에 “PowerShell Gallery(www.powershellgallery.com)로 나갈 때의 프록시 설정”을 확인하세요.
폐쇄망이라 외부로 나갈 수 없으면, 인터넷에 나갈 수 있는 PC에서 다음을 실행하고 만들어진 폴더를 그대로 가져옵니다.
# 반출 측
Save-Module -Name Pester -MinimumVersion 5.0.0 -Path 'D:\modules'
반입 측에서는 D:\modules\Pester를 $env:USERPROFILE\Documents\WindowsPowerShell\Modules 등 모듈 검색 경로에 둡니다. 배치 위치는 $env:PSModulePath로 확인할 수 있습니다.
이 절차를 처음에 문서로 남겨 두면, 담당자가 늘었을 때 시작이 빨라집니다. 테스트 기반이 나아가지 않는 이유가 “Pester가 들어가지 않는다”였던 일은 실제로 흔합니다.
18. 테스트를 갖출 때 정해 두고 싶은 규칙
팀에서 PowerShell 스크립트를 관리한다면, 세세한 작성법보다 먼저 규칙을 정합니다.
예를 들어 이런 규칙입니다.
- 테스트 파일은
*.Tests.ps1로 한다 - 테스트 대상은
$PSScriptRoot에서 불러온다 - 파일 작업 테스트는
$TestDrive를 쓴다 - 삭제, 이동, 알림, API 호출은 원칙적으로
Mock한다 - 날짜는 고정할 수 있게 인수로 뺀다
Describe또는It에Unit,Smoke,RequiresAdmin같은 태그를 붙인다- CI에서는
Unit을 표준 실행한다 - 변경 계열 함수에는 가능한 한
SupportsShouldProcess를 붙인다 - 과거의 문제는 재발 방지 테스트로 남긴다
규칙은 너무 많으면 지켜지지 않습니다. 처음에는 다음 세 가지만으로도 충분합니다.
TestDrive를 쓴다
날짜를 고정한다
위험한 작업은 Mock한다
이 세 가지를 지키는 것만으로 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
- Install a package manager for PowerShell (TLS 1.2와 NuGet 공급자) - Microsoft Learn
- Install-Module (-Proxy / -ProxyCredential) - Microsoft Learn
- PublishTestResults@2 - Azure Pipelines 태스크 레퍼런스
- Workflow syntax for GitHub Actions (jobs.<job_id>.steps[*].shell)
관련 글
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
PowerShell에서 COM과 .NET을 호출하는 실무 ── 스크립트가 닿는 범위를 한 번에 넓히기
PowerShell에서 .NET 클래스를 호출하는 방법, Add-Type으로 C#과 Win32 API를 넣는 방법, COM 조작, Excel 프로세스 잔류와 뒷정리, Office 무인 실행이 지원되지 않는 이유, 5.1과 7의 차이까지 실무 관점...
PowerShell 스크립트의 인수 설계와 모듈화 ── 「돌아가는 스크립트」에서 「남에게 넘길 수 있는 스크립트」로
PowerShell 스크립트를 다른 사람에게 넘길 수 있는 품질로 끌어올리는 절차를 정리합니다. param 블록과 [CmdletBinding()], 입력 검증, 파이프라인 입력, -WhatIf 지원, .psm1 모듈화, 사내 공유와 Git 관리의...
Windows PowerShell 5.1과 PowerShell 7의 차이 ── 사내 스크립트 마이그레이션 실무 가이드
Windows PowerShell 5.1과 PowerShell 7의 관계(공존과 pwsh.exe), 5.1에는 신기능을 추가하지 않는다는 공식 방침, 인코딩 차이로 인한 문자 깨짐, #Requires로 막는 방어, 작업 스케줄러 업데이트까지 마이...
PowerShell로 Excel・CSV 업무 처리를 자동화한다 ── 집계・대조・장표 출력의 실무 레시피
PowerShell로 CSV 집계・대조와 Excel 장표 출력을 자동화하는 실무 레시피입니다. Import-Csv/Export-Csv의 문자 코드 기본값(5.1과 7의 차이), Group-Object 집계, Compare-Object와 해시테이블...
C#(CSharp)에서 PowerShell을 실행하고 결과를 객체로 받는 방법
C#에서 PowerShell을 실행하고 결과를 문자열이 아닌 PSObject로 받는 방법을, PowerShell SDK, AddCommand, AddParameter, BaseObject, Properties, 오류 처리까지 실무 관점에서 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Pester로 가장 먼저 무엇을 테스트하면 되나요?
- 삭제 처리 자체가 아니라, 삭제 대상을 고르는 처리(대상 선정)부터 시작합니다. 운영 스크립트에서는 조건 판단, 출력 형태, 위험한 작업 직전, 외부 의존 네 가지를 우선하면 효과를 보기 쉽습니다. 기존 스크립트를 처음부터 크게 고칠 필요는 없습니다. 위험한 처리 앞에 있는 판단 부분을 함수로 분리하고, 그 반환값을 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 문제처럼 보여도, 실제로는 스크립트 구조가 원인인 경우가 많습니다.