Following my previous post about testing PowerShell with Pester, in this post I show you my way of testing PowerShell with Pester in the real world.
Scenario
Consider the following scenario. You want to smoke-test your application using a bunch of files designed for that. Also, you have different environments, such as the development DEV or UAT environments. Every environment has a name for the environment, like dev
for the development environment, and the number of the environment (for example dev1, uat2…). For each environment, I have a folder with test files to remove.
In the following script, I search in the environment folders and subfolders and look at the name of the file that contains the words tmp
and test
to delete. The CleanupFilesFiter.ps1
is checking if the parameters are valid and verifying if the environment has the expected format; read the folders and subfolders to detect what files it has to delete.
Because the script has to delete for real the files in a production environment, I want to be sure it works as expected. For this reason, I want to test it and the easy way is to use Pestel to mock the files or create the structure I need to test the script.
CleanupFilesFilter.ps1
So, I break down the script to better understand what I’m doing here. First, the parameters require to run the script.
The parameters
Now, the parameters the script is expected are:
- the
targetPath
when it has to search for the files to delete - the
targetEnv
(target environment) to determine what environment folder it has to check - the file creation date
RemoveAfterDate
is used to filter the files to remove after this date
param(
[Parameter(Mandatory=$true)] [string]$targetPath,
[Parameter(Mandatory=$true)] [string]$targetEnv,
[Parameter(Mandatory=$true)]
[ValidateScript({[DateTime]::ParseExact($_, "yyyy-MM-dd HH:mm", $null)})]
[string]$RemoveAfterDate
)
The folder list
In the environment folder, I can have lot of folders. So, I want to limit the search in specific folders listed in this variable.
[string[]]$folderList = "Folder1","Folder2","Folder3","Folder4","Folder5"
Validate the environment
As I said before, the files are in an environment folder. This folder has the conventional short name plus the number of the environment. For example, a valid environment is dev1
or UAT3
. To check if the environment string is valid, I use the Regex for it.
function ValidateEnv([string]$theEnv)
{
if ($theEnv -eq "")
{
Write-Error "Environment is an empty string"
return $false
}
$matchText = "^(dev\d?$|prd\d?$)"
if ($theEnv -notmatch $matchText)
{
Write-Error "$($theEnv) is not a valid environment, please use dev optionally followed by a digit"
return $false
}
return $true
}
Check files to delete
Now, here is where the magic happens. This function checks in a specific folder if there is any file that matches the name or the date. Notice that I define and return a fileList
variable that is a [System.Collections.Generic.List[System.IO.FileInfo]]
. This will be useful when I test the function. If there are some files, the script adds the list of files to the fileList
.
The list
of files from Get-ChildItem
is converted into an array of System.IO.FileInfo
.
function CheckFilesToDelete([string]$EnvPath, [string[]]$fList, [datetime]$date) {
$fileList = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
foreach ($folderName in $fList) {
$path = "$EnvPath\$folderName"
$list = @()
if($date -eq $null) {
$list = [System.IO.FileInfo[]]@(Get-ChildItem -Path $path -File -Recurse |
Where-Object { $_.Name -match '.*Tmp ?Test.*' })
}
else {
$list = [System.IO.FileInfo[]]@(Get-ChildItem "$path" -File -Recurse |
Where-Object { $_.CreationTime -gt $date })
}
if ($list.Count -gt 0) { $fileList.AddRange($list) }
}
return $fileList
}
File deletion
Finally, the last function of the script is for deleting the files using the function CheckFilesToDelete
described above. This function checks for every folder in the environment folder the files to delete and add them in the FilesToDelete
variable. In the following script, the delete command in commented and you will see the list of files to delete. If you want to delete for real, just uncomment that line.
function GetData() {
foreach($folderName in $folderList)
{
$path = "$directoryEnvPath\$folderName"
$checkPath = Test-Path $path
if ($checkPath -eq $false)
{
continue;
}
if($filterByDate -eq $true)
{
($NewerFilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName)) > $null
}
else {
($FilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName)) > $null
}
}
if($filterByDate -eq $true)
{
($NewerFilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName -date $RemoveAfterDate)) > $null
}
if($FilesToDelete.Count)
{
$FilesToDelete
#$FilesToDelete | Remove-Item -Verbose -Recurse
}
else
{
Write-Host "`n No smoke test files found to delete in $($directoryEnvPath) `n" -ForegroundColor Yellow
}
if($NewerFilesToDelete.Count)
{
$NewerFilesToDelete
#$NewerFilesToDelete | Remove-Item -Verbose -Recurse
}
else
{
Write-Host "`n No newer files then ${RemoveAfterDate} found to delete in ${directoryEnvPath} `n" -ForegroundColor Yellow
}
}
The full script
Now, for your convenient, I post here the full script called CleanupFilesFilter.ps1
.
param(
[Parameter(Mandatory=$true)] [string]$targetPath,
[Parameter(Mandatory=$true)] [string]$targetEnv,
[Parameter(Mandatory=$true)]
[ValidateScript({[DateTime]::ParseExact($_, "yyyy-MM-dd HH:mm", $null)})]
[string]$RemoveAfterDate
)
$ErrorActionPreference = "Stop"
$filterByDate = $false
[string[]]$folderList = "Folder1","Folder2","Folder3","Folder4","Folder5"
function ValidateEnv([string]$theEnv)
{
if ($theEnv -eq "")
{
Write-Error "Environment is an empty string"
return $false
}
$matchText = "^(dev\d?$|prd\d?$)"
if ($theEnv -notmatch $matchText)
{
Write-Error "$($theEnv) is not a valid environment, please use dev optionally followed by a digit"
return $false
}
return $true
}
function CheckFilesToDelete([string]$EnvPath, [string[]]$fList, [datetime]$date) {
$fileList = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
foreach ($folderName in $fList) {
$path = "$EnvPath\$folderName"
$list = @()
if($date -eq $null) {
$list = [System.IO.FileInfo[]]@(Get-ChildItem -Path $path -File -Recurse |
Where-Object { $_.Name -match '.*Tmp ?Test.*' })
}
else {
$list = [System.IO.FileInfo[]]@(Get-ChildItem "$path" -File -Recurse |
Where-Object { $_.CreationTime -gt $date })
}
if ($list.Count -gt 0) { $fileList.AddRange($list) }
}
return $fileList
}
function GetData() {
foreach($folderName in $folderList)
{
$path = "$directoryEnvPath\$folderName"
$checkPath = Test-Path $path
if ($checkPath -eq $false)
{
continue;
}
if($filterByDate -eq $true)
{
($NewerFilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName)) > $null
}
else {
($FilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName)) > $null
}
}
if($filterByDate -eq $true)
{
($NewerFilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName -date $RemoveAfterDate)) > $null
}
if($FilesToDelete.Count)
{
$FilesToDelete
#$FilesToDelete | Remove-Item -Verbose -Recurse
}
else
{
Write-Host "`n No smoke test files found to delete in $($directoryEnvPath) `n" -ForegroundColor Yellow
}
if($NewerFilesToDelete.Count)
{
$NewerFilesToDelete
#$NewerFilesToDelete | Remove-Item -Verbose -Recurse
}
else
{
Write-Host "`n No newer files then ${RemoveAfterDate} found to delete in ${directoryEnvPath} `n" -ForegroundColor Yellow
}
}
#
# Start
Write-Host "This script will delete files and folders created by testing"
ValidateEnv $targetEnv > $null
$targetPathEnv = "$targetPath\$targetEnv"
$checkPath = Test-Path $targetPathEnv
if ($checkPath -eq $false)
{
Write-Error "$($targetPathEnv) doesn't exist"
}
if($RemoveAfterDate -ne "")
{
$filterByDate = $true
}
$directoryEnvPath = "$targetPathEnv\TestFolder"
[System.IO.DirectoryInfo[]]$FilesToDelete
GetData
CleanupFilesFilter.Tests.ps1
Now, I explained in the previous post, it is common for a PowerShell script test to have a file with the same name of the file to test plus .Test
before the extension.
BeforeAll
So, this section is called from Pester before anything else. Therefore, here is the right place to add the code to create folders and files for testing the script.
BeforeAll {
$path = Join-Path -Path $targetPath -ChildPath $targetEnv
Write-Host "This is the path $path"
Write-Host ""
Write-Host "Creating folders and files..."
$path = Join-Path -Path $path -ChildPath $mainFolder
$folderList |
ForEach-Object {
$tmpPath = Join-Path $path $_
New-Item $tmpPath -ItemType Directory -force
("TmpTest","Tmp Test","tmp-test","Tmp 1Test","test") | foreach { New-Item -Path $tmpPath -Name "$_.txt" -Force }
}
Write-Host "Creation completed"
. $PSCommandPath.Replace('.Tests.ps1', '.ps1') -targetPath $targetPath -targetEnv $targetEnv -RemoveAfterDate $RemoveAfterDate
}
As you can see, with New-Item
I force the creation of the directory and then with the same command force the creation of same files in the folder. When the creation process is completed, I run the script I want to test.
Validate the environment
First, I want to test the function ValidateEnv
to check if it returns what I expect with different input. Remember, the environment has to have the environment code plus a number.
Describe "Validate Environment" {
Context "when environment is empty" {
It "should return 'Environment is an empty string'" {
$scriptBlock = { ValidateEnv -theEnv '' -ErrorAction Stop }
$scriptBlock | Should -Throw "Environment is an empty string"
}
}
Context "when environment has more than 1 digit" {
It "should return 'Environment is an empty string'" {
$tmpEnv = 'dev01'
$scriptBlock = { ValidateEnv -theEnv $tmpEnv -ErrorAction Stop }
$scriptBlock | Should -Throw "$($tmpEnv) is not a valid environment, please use dev optionally followed by a digit"
}
}
In case the ValidateEnv
finds the name of the environment is wrong, it raise an error with Write-Error... return $false
. This causes an issue also in the test script. For this reason, I define the variable scriptBlock
and trap the error and compare the returned string.
Check files
Then, this is the most complicated part of the all scripts. Because I want to verify if the function picks up the right files, I have to create an object in PowerShell and Pester for a file with all the details.
Mock Get-ChildItem
Now, let me start with the simple one. Because I want to test different thing, I need different files. Pester provides a Mock
for a lot of commands. In particular, I have to mock the result of Get-ChildItem
that contains in this case file details.
Mock Get-ChildItem {
$arr = @(
[System.IO.FileInfo]::new ('Tmp 1Test.txt'),
[System.IO.FileInfo]::new ('tmp-test.txt'),
[System.IO.FileInfo]::new ('TmpTest.txt'),
[System.IO.FileInfo]::new ('qq02000.doc'))
In this code, the Mock
for Get-ChildItem
has an array arr
with 4 files only with their names. So, I apply the filter on this Mock
with
$arr | Where-Object { $_.Name -match '.*Tmp ?Test.*' }
that extracts from the list only the files with Tmp Test
in the file name. Then, I define a variable to list the files I expected to have from the function I want to test.
$expected = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
$expected.Add([System.IO.FileInfo]::new('TmpTest.txt'))
Now, I’m calling the function CheckFilesToDelete
and it returns a list
of files. Then, I sort the list of files and take only the name of the files not all the attributes and check the list with the expected
list.
$list = CheckFilesToDelete -EnvPath $path -fList $folderList -ErrorAction Stop
$list.Name | Sort-Object | Should -Be ($expected.Name | Sort-Object Name)
If the list
has the same files of expected
, the test is passed. And it passed.
Complex Mock Get-ChildItem
So far so good. My initial idea was to test the creation date of files to decide if delete them or not. In PowerShell I couldn’t find a way to create a file and change the creation date and time. For this reason, I have to create same MockObject
to add in the Mock Get-ChildItem
.
Mock Get-ChildItem {
$arr = @(
(New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'Tmp Test.txt'; CreationTime = [datetime]'2023-01-01 21:00:00' }),
(New-MockObject - Type 'System.IO.FileInfo' - Properties @{ Name = 'tmp-test.txt'; CreationTime = [datetime]'2023-01-01 22:00:00' }),
(New-MockObject - Type 'System.IO.FileInfo' - Properties @{ Name = 'TmpTest.txt'; CreationTime = [datetime]'2022-01-01 22:00:00' }),
(New-MockObject - Type 'System.IO.FileInfo' - Properties @{ Name = 'qq02000.doc'; CreationTime = [datetime]'2020-01-01 22:15:00' })
)
So, using here I use the Pester command New-MockObject
to create a list of files and add the CreationTime
I want. So, I can test also the part of the function when I want to test the date of the files.
The full test script
# Define variables
$targetPath = $PSScriptRoot
$targetEnv = 'dev1'
$mainFolder = "TestFolder"
$RemoveAfterDate = "2022-01-01 00:00"
[string[]]$folderList = "Folder1","Folder2","Folder3","Folder4","Folder5"
# Setup the tests environment
BeforeAll {
$path = Join-Path -Path $targetPath -ChildPath $targetEnv
Write-Host "This is the path $path"
Write-Host ""
Write-Host "Creating folders and files..."
$path = Join-Path -Path $path -ChildPath $mainFolder
$folderList |
ForEach-Object {
$tmpPath = Join-Path $path $_
New-Item $tmpPath -ItemType Directory -force
("TmpTest","Tmp Test","tmp-test","Tmp 1Test","test") | foreach { New-Item -Path $tmpPath -Name "$_.txt" -Force }
}
Write-Host "Creation completed"
. $PSCommandPath.Replace('.Tests.ps1', '.ps1') -targetPath $targetPath -targetEnv $targetEnv -RemoveAfterDate $RemoveAfterDate
}
# Validate environment function
Describe "Validate Environment" {
Context "when environment is empty" {
It "should return 'Environment is an empty string'" {
$scriptBlock = { ValidateEnv -theEnv '' -ErrorAction Stop }
$scriptBlock | Should -Throw "Environment is an empty string"
}
}
Context "when environment has more than 1 digit" {
It "should return 'Environment is an empty string'" {
$tmpEnv = 'dev01'
$scriptBlock = { ValidateEnv -theEnv $tmpEnv -ErrorAction Stop }
$scriptBlock | Should -Throw "$($tmpEnv) is not a valid environment, please use dev optionally followed by a digit"
}
}
Context "when environment is valid" {
It "should return true" {
ValidateEnv -theEnv $targetEnv | Should -Be $true
}
}
}
Describe "Validate files to delete" {
Context "validate files with name" {
It "should return a list of expected files (mock)" {
[string[]]$folderList = "Tests"
$expected = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
$expected.Add([System.IO.FileInfo]::new('TmpTest.txt'))
Mock Get-ChildItem {
$arr = @(
[System.IO.FileInfo]::new('Tmp 1Test.txt'),
[System.IO.FileInfo]::new('tmp-test.txt'),
[System.IO.FileInfo]::new('TmpTest.txt'),
[System.IO.FileInfo]::new('qq02000.doc'))
# Mocking the -Filter parameter
$arr | Where-Object { $_.Name -match '.*Tmp ?Test.*' }
}
$list = CheckFilesToDelete -EnvPath $path -fList $folderList -ErrorAction Stop
$list.Name | Sort-Object | Should -Be ($expected.Name | Sort-Object Name)
}
It "should return a list of expected files (file system)" {
[System.Collections.Generic.List[System.Object]]$expected = @(
[PSCustomObject]@{ Name = 'Tmp Test.txt' },
[PSCustomObject]@{ Name = 'tmp-test.txt' }
)
$list = CheckFilesToDelete -EnvPath $path -fList $folderList -ErrorAction Stop
$list.Count | Should -Be 10
$list[0].Name | Should -Be 'Tmp Test.txt'
}
}
Context "validate files with date" {
It "should return a list of expected files (mock)" {
[string[]]$folderList = "Tests"
$expected = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
$expected.Add([System.IO.FileInfo]::new('TmpTest.txt'))
$expected.Add([System.IO.FileInfo]::new('Tmp Test.txt'))
Mock Get-ChildItem {
$arr = @(
(New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'Tmp Test.txt'; CreationTime = [datetime]'2023-01-01 21:00:00' }),
(New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'tmp-test.txt'; CreationTime = [datetime]'2023-01-01 22:00:00' }),
(New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'TmpTest.txt'; CreationTime = [datetime]'2022-01-01 22:00:00' }),
(New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'qq02000.doc'; CreationTime = [datetime]'2020-01-01 22:15:00' })
)
# Mocking the -Filter parameter if used
if ($PesterBoundParameters.Filter) {
return $arr | Where-Object Name -Like $PesterBoundParameters.Filter
}
return $arr
}
$dt = [DateTime]::ParseExact('2023-01-01 00:00', 'yyyy-MM-dd HH:mm', $null)
$list = CheckFilesToDelete -EnvPath $path -fList $folderList -date $dt -ErrorAction Stop
$list.Count | Should -Be 2
$list[0].Name | Should -Be 'Tmp Test.txt'
}
}
}
The result in PowerShell
Finally, here the screenshot of Windows PowerShell SE and the output of the tests.
Wrap up
In conclusion, this is a real example of testing PowerShell with Pester for a real world. I’m using this script in a production environment and the test with Pester is very useful. Please let me know what you think about.
Happy coding!