Public/New-PesterCustomTests.ps1
|
<# .SYNOPSIS Creates Pester 5 test files for one or more PowerShell function scripts. .DESCRIPTION New-PesterCustomTests generates a <Name>.Tests.ps1 file for each function script in Source. Source can be a single .ps1 file or a folder (its *.ps1 files, excluding *.Tests.ps1). Test files are written to Destination, which defaults to a 'Tests' folder next to the source files. Each generated test imports the module that contains it (it walks up from the test file to the folder that holds <ModuleName>.psd1), keeps tcs.core settings, update checks and telemetry away from the real profile and network, and checks that the function exists and has help. Add behaviour tests to the generated Describe block. PSScriptAnalyzer checks belong in a module-level test or in CI, so no per-file analyzer tests or settings files are generated. Existing test files are left alone unless -Force is used. .PARAMETER Source Path to a single .ps1 file or a folder containing .ps1 files to generate tests for. Defaults to the current location. Accepts pipeline input by property name (Path, FullName). .PARAMETER Destination Folder where the test file(s) will be created. Created if it does not exist. Defaults to a folder named 'Tests' in the source folder (or next to the source file). .PARAMETER Force Overwrite test files that already exist. .EXAMPLE New-PesterCustomTests -Source './MyModule/Public' Creates ./MyModule/Public/Tests/<Name>.Tests.ps1 for each .ps1 file in ./MyModule/Public. .EXAMPLE Get-ChildItem -Path './MyModule/Private' -Filter '*.ps1' | New-PesterCustomTests -Destination './MyModule/Private/Tests' Creates tests for each piped private function script. .INPUTS System.String or System.IO.FileInfo. Path to a file or folder (Source), by property name. .OUTPUTS None. Writes .Tests.ps1 files to the destination folder. .NOTES Author: TheCodeSaiyan #> function New-PesterCustomTests { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Public command name kept for backward compatibility.')] [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([void])] param ( [Parameter(ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('Path', 'FullName')] [PSDefaultValue(Help = 'The current location')] [ValidateScript({ if (-not (Test-Path -Path $_)) { throw "File or folder '$_' does not exist." } return $true })] [string] $Source = $PWD.Path, [ValidateNotNullOrEmpty()] [PSDefaultValue(Help = 'A Tests folder in the source folder')] [string] $Destination, [switch] $Force ) begin { $TelemetryArgs = @{ ModuleName = $MyInvocation.MyCommand.Module.Name ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version CommandName = $MyInvocation.MyCommand.Name ExecutionID = [guid]::NewGuid().ToString() } Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer $Failures = 0 $PesterPublicTemplate = @' BeforeAll { # Keep tcs.core settings, update checks and telemetry away from the real profile and network $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config' $env:TCS_SKIP_UPDATE_CHECK = '1' $env:TCS_TELEMETRY_OPTOUT = '1' # Import the module that contains this test (the nearest folder holding <FolderName>.psd1) $ModuleRoot = $PSScriptRoot while ($ModuleRoot -and -not (Test-Path -Path (Join-Path -Path $ModuleRoot -ChildPath ((Split-Path -Path $ModuleRoot -Leaf) + '.psd1')))) { $ModuleRoot = Split-Path -Path $ModuleRoot -Parent } if (-not $ModuleRoot) { throw "No module manifest was found in or above '$PSScriptRoot'." } $ModuleName = Split-Path -Path $ModuleRoot -Leaf Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath "$ModuleName.psd1") -Force } AfterAll { Remove-Module -Name $ModuleName -Force -ErrorAction SilentlyContinue } Describe '###TEMPLATE_FUNCTION_NAME' { It 'Is exported by the module' { Get-Command -Name '###TEMPLATE_FUNCTION_NAME' -Module $ModuleName -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty } It 'Has help with a synopsis and an example' { $help = Get-Help -Name '###TEMPLATE_FUNCTION_NAME' -Full $help.Synopsis | Should -Not -BeNullOrEmpty @($help.Examples.Example).Count | Should -BeGreaterThan 0 } # Add behaviour tests here. Mock commands the function calls with: Mock -ModuleName $ModuleName <Command> { } } '@ $PesterPrivateTemplate = @' BeforeAll { # Keep tcs.core settings, update checks and telemetry away from the real profile and network $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config' $env:TCS_SKIP_UPDATE_CHECK = '1' $env:TCS_TELEMETRY_OPTOUT = '1' # Import the module that contains this test (the nearest folder holding <FolderName>.psd1) $ModuleRoot = $PSScriptRoot while ($ModuleRoot -and -not (Test-Path -Path (Join-Path -Path $ModuleRoot -ChildPath ((Split-Path -Path $ModuleRoot -Leaf) + '.psd1')))) { $ModuleRoot = Split-Path -Path $ModuleRoot -Parent } if (-not $ModuleRoot) { throw "No module manifest was found in or above '$PSScriptRoot'." } $ModuleName = Split-Path -Path $ModuleRoot -Leaf Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath "$ModuleName.psd1") -Force } AfterAll { Remove-Module -Name $ModuleName -Force -ErrorAction SilentlyContinue } Describe '###TEMPLATE_FUNCTION_NAME' { It 'Is defined in the module and not exported' { InModuleScope -ModuleName $ModuleName { Get-Command -Name '###TEMPLATE_FUNCTION_NAME' -CommandType Function -ErrorAction SilentlyContinue } | Should -Not -BeNullOrEmpty Get-Command -Name '###TEMPLATE_FUNCTION_NAME' -Module $ModuleName -ErrorAction SilentlyContinue | Should -BeNullOrEmpty } # Add behaviour tests here, calling the function inside InModuleScope -ModuleName $ModuleName { } } '@ } process { try { $SourceItem = Get-Item -Path $Source -ErrorAction Stop if ($SourceItem.PSIsContainer) { $Files = @(Get-ChildItem -Path $SourceItem.FullName -Filter '*.ps1' -File -ErrorAction Stop | Where-Object { $_.Name -notlike '*.Tests.ps1' }) $TargetFolder = if ($Destination) { $Destination } else { Join-Path -Path $SourceItem.FullName -ChildPath 'Tests' } Write-Verbose "Found $($Files.Count) function file(s) in '$($SourceItem.FullName)'." } elseif ($SourceItem.Extension -eq '.ps1') { $Files = @($SourceItem) $TargetFolder = if ($Destination) { $Destination } else { Join-Path -Path $SourceItem.DirectoryName -ChildPath 'Tests' } } else { throw "Source '$($SourceItem.FullName)' is not a PowerShell script (.ps1) or a folder." } if ($Files.Count -eq 0) { Write-Warning "No .ps1 files were found in '$($SourceItem.FullName)'." return } if (-not (Test-Path -Path $TargetFolder -PathType Container)) { if ($PSCmdlet.ShouldProcess($TargetFolder, 'Create test folder')) { $null = New-Item -Path $TargetFolder -ItemType Directory -Force -ErrorAction Stop } } foreach ($File in $Files) { $TestPath = Join-Path -Path $TargetFolder -ChildPath "$($File.BaseName).Tests.ps1" if ((Test-Path -Path $TestPath) -and -not $Force) { Write-Warning "'$TestPath' already exists and was not changed. Use -Force to overwrite it." continue } if (-not $PSCmdlet.ShouldProcess($TestPath, 'Create Pester test file')) { continue } $Template = if ($File.Directory.Name -eq 'Private') { $PesterPrivateTemplate } else { $PesterPublicTemplate } $Content = $Template.Replace('###TEMPLATE_FUNCTION_NAME', $File.BaseName) Set-Content -Path $TestPath -Value $Content -Encoding UTF8 -Force -ErrorAction Stop Write-Verbose "Created '$TestPath'." } } catch { $Failures++ Write-Error "Failed to create Pester tests for '$Source': $($_.Exception.Message)" } } end { Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed ($Failures -gt 0) } } |