├── .gitignore ├── ExampleTests └── AppX.Tests.ps1 ├── Modules ├── ActiveDirectory.psm1 ├── AzureResourceGroup.psm1 ├── Dns.psd1 ├── Dns.psm1 ├── File.psm1 ├── IISServer.psm1 ├── NetworkAdapter.psm1 ├── Node-VirtualMachine-VirtualMachineManager.psd1 ├── Node-VirtualMachine-VirtualMachineManager.psm1 ├── Node.psm1 ├── PesterSpec.psd1 ├── PesterSpec.psm1 ├── Service.psm1 ├── VirtualMachine.psd1 ├── VirtualMachine.psm1 └── WindowsFeature.psm1 ├── PesterSpec.psprojs ├── README.md ├── Tests ├── File.Tests.ps1 └── Service.Tests.ps1 └── readme-image.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.psproj 3 | 4 | *.psprojs 5 | 6 | *.psprojs 7 | 8 | *.psprojs 9 | -------------------------------------------------------------------------------- /ExampleTests/AppX.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Describe 'Functionality 1' { 2 | # Service -Node DC -Name w32time -Exists 3 | #} 4 | # 5 | #Describe 'Functionality 2' { 6 | # Service -Node DC -Name w32time -State Running 7 | #} 8 | # 9 | #Describe 'Functionality 3' { 10 | # Service -Node DC -Name w32time -StartMode Auto 11 | #} 12 | # 13 | #Describe 'Functionality 4' { 14 | # File -Node DC -Path 'C:\MMASetup-AMD64.exe' -Exists 15 | #} 16 | # 17 | #Describe 'Functionality 5' { 18 | # File -Node DC -Path 'C:\MMASetup-AMD64.exe' -SizeInBytes 28699832 19 | #} 20 | # 21 | #Describe 'AD test' { 22 | # ActiveDirectoryGroup -Name 'Group 1' -DomainName 'mylab.local' 23 | # ActiveDirectoryGroup -Name 'Group 1' -DomainName 'mylab.local' -Scope 'Universal' 24 | # ActiveDirectoryGroup -Name 'Group 1' -DomainName 'mylab.local' -Type 'Security' 25 | #} 26 | # 27 | #describe 'DNS record test' { 28 | # DNSRecord -Name 'DC' -Server 'DC' -ZoneName 'mylab.local' 29 | # DNSRecord -Name 'DC' -Server 'DC' -ZoneName 'mylab.local' -Type 'CNAME' 30 | # DNSRecord -Name 'DC' -Server 'DC' -ZoneName 'mylab.local' -Type 'A' 31 | # DNSRecord -Name 'DC' -Server 'DC' -ZoneName 'mylab.local' -IPV4Address '192.168.1.33' 32 | # DNSRecord -Name 'DC' -Server 'DC' -ZoneName 'mylab.local' -IPV4Address '192.168.0.120' 33 | # DNSRecord -Name 'DC' -Server 'DC' -ZoneName 'mylab.local' -IPV4Address '192.168.0.120' -Type 'A' 34 | # DNSRecord -Name 'DC' -Server 'DC' -ZoneName 'mylab.local' -IPV4Address '192.168.0.120' -Type '' 35 | #} 36 | # 37 | #describe 'DNS zone test' { 38 | # DnsZone -Name 'mylab.local' -Server 'DC' 39 | # DnsZone -Name 'mylab.local' -Server 'DC' -Type 'Primary' 40 | # DnsZone -Name 'myladdb.local' -Server 'DC' 41 | #} 42 | 43 | #Describe 'node test' { 44 | # Node -HostName 'DC' 45 | #} 46 | 47 | #describe 'VMM test no host' { 48 | # VirtualMachine -Type VMM -Name 'BAPP85LSS02' 49 | #} 50 | # 51 | #describe 'VMM test with host' { 52 | # VirtualMachine -Type VMM -Name 'BAPP85LSS02' -VMHost '' 53 | #} 54 | # 55 | #describe 'Azure VM test' { 56 | # VirtualMachine -Type Azure -Name 'PesterTest-01' -ResourceGroupName 'PesterTesting' 57 | #} 58 | 59 | #describe 'IIS test' { 60 | # IISServer -ComputerName 'BAPP85LSS02.bdt085.local' 61 | #} 62 | # 63 | #describe 'Windows feature test' { 64 | # WindowsFeature -ComputerName '' -Name 'Web-Server' 65 | #} 66 | 67 | describe 'AzureResourceGroup test' { 68 | AzureResourceGroup -Name 'PesterTesting' 69 | } 70 | -------------------------------------------------------------------------------- /Modules/ActiveDirectory.psm1: -------------------------------------------------------------------------------- 1 | #region Helper functions 2 | 3 | function ConvertTo-DistinguishedName 4 | { 5 | <# 6 | .SYNOPSIS 7 | This function converts a canonical name to a distinguished name 8 | 9 | .PARAMETER DomainName 10 | The domain name in FQDN format. 11 | 12 | .PARAMETER OUPath 13 | The organization unit path as a canonical name. Canonical names sepaarate OUs by a forward flash. 14 | 15 | .EXAMPLE 16 | PS> ConvertTo-DistinguishedName -DomainName bdt007.local -OUPath Users/Admin Users 17 | 18 | This will output the string OU=Users,OU=Admin Users,DC=bdt007,DC=local 19 | #> 20 | 21 | [CmdletBinding()] 22 | [OutputType([string])] 23 | param 24 | ( 25 | [Parameter(Mandatory)] 26 | [ValidateNotNullOrEmpty()] 27 | [ValidatePattern('^\w+\.\w+$')] 28 | [string]$DomainName, 29 | 30 | [Parameter()] 31 | [ValidateNotNullOrEmpty()] 32 | [string]$OUPath 33 | ) 34 | 35 | $domainSplit = $DomainName.Split('.') 36 | 37 | if ($PSBoundParameters.ContainsKey('OUPath')) 38 | { 39 | $OUPath = $OUPath.Replace('\', '/') 40 | $ouSplit = $OUPath.Split('/') 41 | "OU=$($ouSplit -join ',OU='),DC=$($domainSplit -join ',DC=')" 42 | } 43 | else 44 | { 45 | "DC=$($domainSplit -join ',DC=')" 46 | } 47 | } 48 | 49 | #endregion 50 | 51 | function ActiveDirectoryGroup 52 | { 53 | [CmdletBinding()] 54 | param 55 | ( 56 | [Parameter(Mandatory)] 57 | [ValidateNotNullOrEmpty()] 58 | [string]$Name, 59 | 60 | [Parameter(Mandatory)] 61 | [ValidateNotNullOrEmpty()] 62 | [ValidatePattern('^\w+\.\w+$')] 63 | [string]$DomainName, 64 | 65 | [Parameter()] 66 | [ValidateNotNullOrEmpty()] 67 | [string]$OrganizationalUnit, 68 | 69 | [Parameter()] 70 | [ValidateNotNullOrEmpty()] 71 | [ValidateSet('Distribution', 'Security')] 72 | [string]$Type, 73 | 74 | [Parameter()] 75 | [ValidateNotNullOrEmpty()] 76 | [ValidateSet('Universal', 'Global', 'DomainLocal')] 77 | [string]$Scope, 78 | 79 | [Parameter()] 80 | [ValidateNotNullOrEmpty()] 81 | [string]$Description 82 | ) 83 | 84 | $dnParams = @{ 85 | 'DomainName' = $DomainName 86 | } 87 | 88 | if ($PSBoundParameters.ContainsKey('OrganizationalUnit')) 89 | { 90 | $dnParams.OrganizationalUnit = $OrganizationalUnit 91 | } 92 | 93 | $dn = ConvertTo-DistinguishedName @dnParams 94 | $searchRoot = "LDAP://$dn" 95 | 96 | $scriptBlock = { 97 | $searcher = [adsisearcher]"(&(objectCategory=Group)(name=$using:Name))" 98 | $searcher.SearchRoot = $using:searchRoot 99 | $searcher.FindOne().Properties 100 | } 101 | 102 | $group = Invoke-Command -ComputerName $DomainName -ScriptBlock $scriptBlock 103 | 104 | if (-not $PSBoundParameters.ContainsKey('Type') -and -not $PSBoundParameters.ContainsKey('Scope')) 105 | { 106 | it "group [$($Name)] exists in domain [$($DomainName)]" { 107 | $group | Should not be $null 108 | } 109 | } 110 | 111 | if ($PSBoundParameters.ContainsKey('Type')) { 112 | it "group [$($Name)] should have type of [$($Type)]" { 113 | switch ($group.grouptype) 114 | { 115 | { $_ -in @(2, 4, 8) } { 116 | $groupType = 'Distribution' 117 | } 118 | { $_ -in @(-2147483644, -2147483646, -2147483640) } { 119 | $groupType = 'Security' 120 | } 121 | default { 122 | throw "Unknown group type [$($_)]" 123 | } 124 | } 125 | $groupType | Should be $Type 126 | } 127 | } 128 | 129 | if ($PSBoundParameters.ContainsKey('Scope')) { 130 | it "group [$($Name)] should have scope of [$($Scope)]" { 131 | switch ($group.grouptype) 132 | { 133 | { $_ -in @(2,-2147483646) } { 134 | $groupScope = 'Global' 135 | } 136 | { $_ -in @(4,-2147483644) } { 137 | $groupScope = 'DomainLocal' 138 | } 139 | { $_ -in @(8,-2147483640) } { 140 | $groupScope = 'Universal' 141 | } 142 | default 143 | { 144 | throw "Unknown group scope [$($_)]" 145 | } 146 | } 147 | $groupScope | Should be $Scope 148 | } 149 | } 150 | } 151 | 152 | function ActiveDirectoryUser 153 | { 154 | [CmdletBinding()] 155 | param 156 | ( 157 | [Parameter()] 158 | [ValidateNotNullOrEmpty()] 159 | [string]$MemberOf 160 | 161 | ) 162 | 163 | $Group = [ADSI]"LDAP://cn=Domain Admins,cn=Users,dc=Contoso,dc=Com" 164 | $Members = $Group.Member | ForEach-Object { [ADSI]"LDAP://$_" } 165 | 166 | } 167 | 168 | function ActiveDirectoryDomain 169 | { 170 | [CmdletBinding()] 171 | param 172 | ( 173 | 174 | ) 175 | 176 | } 177 | 178 | function ActiveDirectoryForest 179 | { 180 | [CmdletBinding()] 181 | param 182 | ( 183 | 184 | ) 185 | 186 | } 187 | 188 | Export-ModuleMember -Function 'ActiveDirectory*' -------------------------------------------------------------------------------- /Modules/AzureResourceGroup.psm1: -------------------------------------------------------------------------------- 1 | function AzureResourceGroup 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [Parameter()] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$Name 9 | ) 10 | 11 | it "an Azure resource group [$($Name)] should exist" { 12 | $null = Get-AzureRmResourceGroup -Name $Name -ev err -ea SilentlyContinue 13 | $err.exception | Should not match 'Provided resource group does not exist\.' 14 | } 15 | } -------------------------------------------------------------------------------- /Modules/Dns.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/PesterSpec/28951e4b777c6959b322f7fb98b5e4cfa5321c75/Modules/Dns.psd1 -------------------------------------------------------------------------------- /Modules/Dns.psm1: -------------------------------------------------------------------------------- 1 | function DnsZone 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [Parameter(Mandatory)] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$Name, 9 | 10 | [Parameter(Mandatory)] 11 | [ValidateNotNullOrEmpty()] 12 | [string]$Server, 13 | 14 | [Parameter()] 15 | [ValidateNotNullOrEmpty()] 16 | [ValidateSet('Primary')] 17 | [string]$Type 18 | 19 | ) 20 | 21 | $scriptBlock = { 22 | Get-DnsServerZone -Name $using:Name -ErrorAction 'SilentlyContinue' 23 | } 24 | 25 | $zone = Invoke-Command -ComputerName $Server -ScriptBlock $scriptBlock -HideComputerName 26 | 27 | if (-not $PSBoundParameters.ContainsKey('Type')) { 28 | it "DNS zone [$($Name)] exists on server [$($Server)]" { 29 | $zone | Should not be $null 30 | } 31 | } 32 | 33 | if ($PSBoundParameters.ContainsKey('Type')) 34 | { 35 | it "DNS zone [$($Name)] on server [$($Server)] is type [$($Type)]" { 36 | $zone.ZoneType | Should be $Type 37 | } 38 | } 39 | } 40 | 41 | function DnsRecord 42 | { 43 | [CmdletBinding()] 44 | param 45 | ( 46 | [Parameter(Mandatory)] 47 | [ValidateNotNullOrEmpty()] 48 | [string]$Name, 49 | 50 | [Parameter(Mandatory)] 51 | [ValidateNotNullOrEmpty()] 52 | [string]$ZoneName, 53 | 54 | [Parameter(Mandatory)] 55 | [ValidateNotNullOrEmpty()] 56 | [string]$Server, 57 | 58 | [Parameter()] 59 | [ValidateNotNullOrEmpty()] 60 | [string]$IpV4Address, 61 | 62 | [Parameter()] 63 | [ValidateNotNullOrEmpty()] 64 | [ValidateSet('A', 'CNAME')] 65 | [string]$Type 66 | ) 67 | 68 | $scriptBlock = { 69 | Get-DnsServerResourceRecord -Name $using:Name -ZoneName $using:ZoneName 70 | } 71 | 72 | $record = Invoke-Command -ComputerName $Server -ScriptBlock $scriptBlock -HideComputerName 73 | 74 | if ($PSBoundParameters.ContainsKey('Type')) 75 | { 76 | it "DNS record [$($Name)] is in zone [$($ZoneName)] and is type [$($Type)]" { 77 | $record.RecordType | Should be $Type 78 | } 79 | } 80 | 81 | if ($PSBoundParameters.ContainsKey('IpV4Address')) 82 | { 83 | it "DNS record [$($Name)] is in zone [$($ZoneName)] and has IPV4 address [$($IpV4Address)]" { 84 | $record.RecordData.IPv4Address.IPAddressToString | Should be $IpV4Address 85 | } 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /Modules/File.psm1: -------------------------------------------------------------------------------- 1 | function File 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [Parameter(Mandatory)] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$Path, 9 | 10 | [Parameter()] 11 | [ValidateNotNullOrEmpty()] 12 | [string]$Node, 13 | 14 | [Parameter()] 15 | [ValidateNotNullOrEmpty()] 16 | [int]$SizeInBytes, 17 | 18 | [Parameter()] 19 | [ValidateNotNullOrEmpty()] 20 | [pscredential]$Credential 21 | ) 22 | 23 | if ($PSBoundParameters.ContainsKey('Node')) { 24 | $icmParams = @{ 25 | 'ComputerName' = $Node 26 | } 27 | if ($PSBoundParameters.ContainsKey('Credential')) 28 | { 29 | $icmParams.Credential = $Credential 30 | } 31 | 32 | $file = Invoke-Command @icmParams -ScriptBlock { Get-Item -Path $using:Path -ErrorAction Ignore } 33 | } 34 | else 35 | { 36 | $file = Get-Item -Path $Path -ErrorAction Ignore 37 | } 38 | 39 | it "The file [$($Path)] exists." { 40 | $file | Should not BeNullOrEmpty 41 | } 42 | 43 | if ($PSBoundParameters.ContainsKey('SizeInBytes')) 44 | { 45 | it "The file [$($Path)] is [$($SizeInBytes)] bytes" { 46 | $file.Length | Should be $SizeInBytes 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Modules/IISServer.psm1: -------------------------------------------------------------------------------- 1 | function IISServer 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [Parameter(Mandatory)] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$ComputerName 9 | 10 | ) 11 | 12 | $scriptBlock = { 13 | (Get-WindowsFeature -Name 'Web-Server').Installed 14 | } 15 | 16 | it "Computer [$($ComputerName)] should have the web server feature installed" { 17 | Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock | Should be $true 18 | } 19 | } -------------------------------------------------------------------------------- /Modules/NetworkAdapter.psm1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/PesterSpec/28951e4b777c6959b322f7fb98b5e4cfa5321c75/Modules/NetworkAdapter.psm1 -------------------------------------------------------------------------------- /Modules/Node-VirtualMachine-VirtualMachineManager.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/PesterSpec/28951e4b777c6959b322f7fb98b5e4cfa5321c75/Modules/Node-VirtualMachine-VirtualMachineManager.psd1 -------------------------------------------------------------------------------- /Modules/Node-VirtualMachine-VirtualMachineManager.psm1: -------------------------------------------------------------------------------- 1 | function VirtualMachine-VirtualMachineManager 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [Parameter(Mandatory)] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$Name, 9 | 10 | [Parameter(Mandatory)] 11 | [ValidateNotNullOrEmpty()] 12 | [string]$VMHost, 13 | 14 | [Parameter()] 15 | [ValidateNotNullOrEmpty()] 16 | [ValidateSet('Running', 'PoweredOff')] 17 | [string]$Status, 18 | 19 | [Parameter()] 20 | [ValidateNotNullOrEmpty()] 21 | [int]$MemoryMB 22 | ) 23 | 24 | $vm = Get-SCVirtualMachine -VMHost $VMHost -Name $Name 25 | it "the VM [$($Name)] should exist on the VM host [$($VMHost)]" { 26 | $vm | Should not be $null 27 | } 28 | 29 | if ($PSBoundParameters.ContainsKey('Status')) { 30 | it "the VM [$($Name)] should be in the state [$($Status)]" { 31 | $vm.Status | Should be $Status 32 | } 33 | } 34 | 35 | if ($PSBoundParameters.ContainsKey('MemoryMB')) { 36 | it "the VM [$($Name)] should have [$($MemoryMB)] MB of memory" { 37 | $vm.Memory | Should be $MemoryMB 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /Modules/Node.psm1: -------------------------------------------------------------------------------- 1 | function Node 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [Parameter()] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$HostName 9 | ) 10 | 11 | $scriptBlock = { 12 | $env:COMPUTERNAME 13 | } 14 | 15 | it "should have a hostname of [$($HostName)]" { 16 | Invoke-Command -ComputerName $HostName -ScriptBlock $scriptBlock | Should be $HostName 17 | } 18 | } -------------------------------------------------------------------------------- /Modules/PesterSpec.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/PesterSpec/28951e4b777c6959b322f7fb98b5e4cfa5321c75/Modules/PesterSpec.psd1 -------------------------------------------------------------------------------- /Modules/PesterSpec.psm1: -------------------------------------------------------------------------------- 1 | #region Helper functions 2 | 3 | function New-ValidationDynamicParam 4 | { 5 | [CmdletBinding()] 6 | [OutputType('System.Management.Automation.RuntimeDefinedParameter')] 7 | param ( 8 | [Parameter(Mandatory)] 9 | [ValidateNotNullOrEmpty()] 10 | [string]$Name, 11 | 12 | [ValidateNotNullOrEmpty()] 13 | [Parameter()] 14 | [array]$ValidateSetOptions, 15 | 16 | [Parameter()] 17 | [switch]$Mandatory = $false, 18 | 19 | [Parameter()] 20 | [string[]]$ParameterSetName = '__AllParameterSets', 21 | 22 | [Parameter()] 23 | [switch]$ValueFromPipeline = $false, 24 | 25 | [Parameter()] 26 | [switch]$ValueFromPipelineByPropertyName = $false 27 | ) 28 | 29 | $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] 30 | $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute 31 | $ParamAttrib.Mandatory = $Mandatory.IsPresent 32 | $ParamAttrib.ParameterSetName = $ParameterSetName 33 | $ParamAttrib.ValueFromPipeline = $ValueFromPipeline.IsPresent 34 | $ParamAttrib.ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName.IsPresent 35 | $AttribColl.Add($ParamAttrib) 36 | if ($PSBoundParameters.ContainsKey('ValidateSetOptions')) { 37 | $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($ValidateSetOptions))) 38 | } 39 | $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Name, [string], $AttribColl) 40 | $RuntimeParam 41 | 42 | } 43 | 44 | #endregion 45 | 46 | function Start-PesterSpecTest 47 | { 48 | [CmdletBinding()] 49 | param ( 50 | [Parameter(Mandatory)] 51 | [ValidateNotNullOrEmpty()] 52 | [ValidateScript({ Test-Path -Path $_ })] 53 | [string]$Path, 54 | 55 | [Parameter()] 56 | [ValidateNotNullOrEmpty()] 57 | [string]$TestName, 58 | 59 | [Parameter()] 60 | [ValidateNotNullOrEmpty()] 61 | [string[]]$ExcludeTag = 'Disabled', 62 | 63 | [Parameter()] 64 | [ValidateNotNullOrEmpty()] 65 | [string[]]$Tag 66 | ) 67 | 68 | $pesterScrParams = @{ 69 | 'Path' = $Path 70 | } 71 | 72 | $invPesterParams = @{ 73 | 'Script' = $pesterScrParams 74 | 'ExcludeTag' = $ExcludeTag 75 | } 76 | 77 | if ($PSBoundParameters.ContainsKey('Tag')) 78 | { 79 | $invPesterParams.Tag = $Tag 80 | } 81 | 82 | if ($PSBoundParameters.ContainsKey('TestName')) 83 | { 84 | $invPesterParams.TestName = $TestName 85 | } 86 | 87 | Invoke-Pester @invPesterParams 88 | } -------------------------------------------------------------------------------- /Modules/Service.psm1: -------------------------------------------------------------------------------- 1 | function Service 2 | { 3 | [CmdletBinding(DefaultParameterSetName = 'None')] 4 | param 5 | ( 6 | [Parameter(Mandatory)] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$Node, 9 | 10 | [Parameter(Mandatory)] 11 | [ValidateNotNullOrEmpty()] 12 | [string]$Name, 13 | 14 | [Parameter(Mandatory,ParameterSetName = 'State')] 15 | [ValidateNotNullOrEmpty()] 16 | [ValidateSet('Stopped','Running','Disabled')] 17 | [string]$State, 18 | 19 | [Parameter(Mandatory,ParameterSetName = 'StartMode')] 20 | [ValidateNotNullOrEmpty()] 21 | [ValidateSet('Auto','Disabled')] 22 | [string]$StartMode, 23 | 24 | [Parameter(Mandatory,ParameterSetName = 'Exists')] 25 | [ValidateNotNullOrEmpty()] 26 | [switch]$Exists, 27 | 28 | [Parameter()] 29 | [ValidateNotNullOrEmpty()] 30 | [pscredential]$Credential 31 | ) 32 | 33 | $icmParams = @{ 34 | 'ComputerName' = $Node 35 | } 36 | if ($PSBoundParameters.ContainsKey('Credential')) { 37 | $icmParams.Credential = $Credential 38 | } 39 | 40 | $service = Invoke-Command @icmParams -ScriptBlock { Get-CimInstance -ClassName 'Win32_Service' -Filter "Name = '$using:Name'" } 41 | 42 | switch ($PSCmdlet.ParameterSetName) 43 | { 44 | 'Exists' { 45 | it "The service [$($Name)] exists" { 46 | $service | Should not be $null 47 | } 48 | } 49 | 'State' { 50 | it "The service [$($Name)] is [$($State)]" { 51 | $service.State | Should be $State 52 | } 53 | } 54 | 'StartMode' { 55 | it "The service [$($Name)] is set to [$($StartMode)] start mode" { 56 | $service.StartMode | Should be $StartMode 57 | } 58 | } 59 | default 60 | { 61 | throw "Unrecognized parameter set: [$($_)]" 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Modules/VirtualMachine.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/PesterSpec/28951e4b777c6959b322f7fb98b5e4cfa5321c75/Modules/VirtualMachine.psd1 -------------------------------------------------------------------------------- /Modules/VirtualMachine.psm1: -------------------------------------------------------------------------------- 1 | function VirtualMachine 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [Parameter(Mandatory)] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$Name, 9 | 10 | [Parameter(Mandatory)] 11 | [ValidateNotNullOrEmpty()] 12 | [ValidateSet('VMM', 'HyperV', 'VMWare','Azure')] 13 | [string]$Type, 14 | 15 | [Parameter()] 16 | [ValidateNotNullOrEmpty()] 17 | [string]$VMHost, 18 | 19 | [Parameter()] 20 | [ValidateNotNullOrEmpty()] 21 | [ValidateSet('Running', 'PoweredOff')] 22 | [string]$Status, 23 | 24 | [Parameter()] 25 | [ValidateNotNullOrEmpty()] 26 | [int]$MemoryMB 27 | ) 28 | 29 | DynamicParam 30 | { 31 | $ParamOptions = @() 32 | switch ($Type) 33 | { 34 | 'VMM' 35 | { 36 | $ParamOptions += @{ 'Name' = 'VMCPath' } 37 | 38 | $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary 39 | foreach ($Param in $ParamOptions) 40 | { 41 | $RuntimeParam = New-ValidationDynamicParam @Param 42 | $RuntimeParamDic.Add($Param.Name, $RuntimeParam) 43 | } 44 | } 45 | 'HyperV' 46 | { 47 | # 48 | } 49 | 'VMWare' 50 | { 51 | # 52 | } 53 | 'Azure' { 54 | $ParamOptions += @{ 55 | 'Name' = 'ResourceGroupName' 56 | 'Mandatory' = $true 57 | } 58 | } 59 | default 60 | { 61 | throw "Unrecognized virtual machine type: [$($Type)]" 62 | } 63 | } 64 | $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary 65 | foreach ($Param in $ParamOptions) 66 | { 67 | $RuntimeParam = New-ValidationDynamicParam @Param 68 | $RuntimeParamDic.Add($Param.Name, $RuntimeParam) 69 | } 70 | return $RuntimeParamDic 71 | } 72 | 73 | begin 74 | { 75 | $PsBoundParameters.GetEnumerator() | foreach { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue' } 76 | } 77 | 78 | process 79 | { 80 | switch ($Type) 81 | { 82 | 'VMM' { 83 | $vm = Get-SCVirtualMachine -Name $Name 84 | } 85 | 'HyperV' { 86 | throw 'Hyper-V support not added yet' 87 | } 88 | 'VMWare' { 89 | throw 'VMware support not added yet' 90 | } 91 | 'Azure' { 92 | $vm = Get-AzureRmVM -Name $Name -ResourceGroupName $ResourceGroupName -ErrorAction SilentlyContinue 93 | } 94 | default 95 | { 96 | throw "Unrecognized VirtualMachine type: [$($_)]" 97 | } 98 | } 99 | 100 | it "the VM [$($Name)] should exist" { 101 | $vm | Should not be $null 102 | } 103 | 104 | if ($PSBoundParameters.ContainsKey('VMHost')) { 105 | it "the VM [$($Name)] should be on the VM host [$($VMHost)]" { 106 | $vm.VMHost | Should be $VMHost 107 | } 108 | } 109 | 110 | if ($PSBoundParameters.ContainsKey('Status')) 111 | { 112 | it "the VM [$($Name)] should be in the state [$($Status)]" { 113 | $vm.Status | Should be $Status 114 | } 115 | } 116 | 117 | if ($PSBoundParameters.ContainsKey('MemoryMB')) 118 | { 119 | it "the VM [$($Name)] should have [$($MemoryMB)] MB of memory" { 120 | $vm.Memory | Should be $MemoryMB 121 | } 122 | } 123 | 124 | } 125 | } -------------------------------------------------------------------------------- /Modules/WindowsFeature.psm1: -------------------------------------------------------------------------------- 1 | function WindowsFeature 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [Parameter(Mandatory)] 7 | [ValidateNotNullOrEmpty()] 8 | [string]$ComputerName, 9 | 10 | [Parameter(Mandatory)] 11 | [ValidateNotNullOrEmpty()] 12 | [string]$Name, 13 | 14 | [Parameter(Mandatory)] 15 | [ValidateNotNullOrEmpty()] 16 | [ValidateSet('Installed','NotInstalled')] 17 | [string]$Status 18 | ) 19 | 20 | $scriptBlock = { 21 | Get-WindowsFeature -Name $using:Name 22 | } 23 | 24 | it "Computer [$($ComputerName)] has the feature [$($feature.Name)] [$($Status)]" { 25 | switch ($Status) { 26 | 'Installed' { 27 | $feature.Installed | Should be $true 28 | } 29 | 'NotInstalled' { 30 | $feature.Installed | Should be $false 31 | } 32 | default { 33 | # 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /PesterSpec.psprojs: -------------------------------------------------------------------------------- 1 |  2 | 1.0 3 | a5f624b9-51d8-4a94-ae22-92664f016a78 4 | 5 | Modules 6 | ExampleTests 7 | Tests 8 | 9 | 10 | ExampleTests\AppX.Tests.ps1 11 | Modules\VirtualMachine.psm1 12 | Modules\PesterSpec.psm1 13 | Modules\PesterSpec.psd1 14 | Modules\IISServer.psm1 15 | Modules\WindowsFeature.psm1 16 | Modules\Node.psm1 17 | Modules\Node-VirtualMachine-VirtualMachineManager.psd1 18 | Modules\Dns.psm1 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PesterSpec 2 | 3 | > An integration testing framework built in PowerShell around the Pester testing framework for infrastructure automation junkies. 4 | 5 | ## Software Testing Background 6 | Software developers have religiously tested their code for ages now. Testing allows developers to ensure that the code that is produced does what the developer intended without any unforeseen consequences. It's an important part of software development. However, infrastructure folks haven't really caught on yet. Sure, we write scripts and some of us are essentially developers implementing Infrastructure as Code but we're still pretty new to testing. 7 | 8 | In the software testing world, there are various "levels" of testing. These "levels" have to do with how close to the code your tests are run. For example, if you'd like to test the actual code itself, you might use unit tests. If you'd like to test groups of code, you might choose to build functional tests and finally, if you'd like to test the end result, you'd build integration tests. 9 | 10 | Integration tests ensure the end result of the code is met; not how that result came to be. For example, if I'm writing a PowerShell script to delete a file, I don't care if I use Remove-Item or the old-school del command. All I care about is that the file was deleted. I want to test if the file is absent or not. This is what PesterSpec is all about. 11 | 12 | ## PesterSpec's Purpose 13 | 14 | PesterSpec was built to ensure the changes you make to your Windows infrastructure are what you intended. It was built to essentially convert the popular Pester unit testing framework for use with integration tests. With PesterSpec, you don't have to worry about how to use Pester. All you need to know is what configuration items your scripts are changing and into what state. From there, you can write PesterSpec tests just as you would PowerShell functions to verify all the items your scripts are changing are in the state in which you expect. 15 | 16 | ## Prerequisites 17 | 18 | 1. PowerShell v4+ installed on your local computer. 19 | 2. PowerShell v4+ installed on any computer tests will be run against. 20 | 3. PowerShell remoting enabled and all proper firewall exceptions added on any computer tests will be run against. 21 | 4. The Pester module. 22 | 23 | ## Installation 24 | 25 | 1. Download the [Pester module](https://github.com/pester/Pester). 26 | 1. Download the [repo's zip file](https://github.com/adbertram/PesterSpec/archive/master.zip) and extract to a PesterSpec folder in one of the folders in `$env:PSModulePath`. 27 | 28 | ## How-To 29 | 30 | 1. Use `(Get-Command -Module PesterSpec).Name` to discover all available resources. 31 | 2. Use `(Get-Command -Module PesterSpec -Name $ResourceName).Parameters` to see all of the attributes that can be set for that resource. 32 | 3. Modify the example tests provided in ExampleTests to your liking. 33 | 4. Run the test script. 34 | 35 | `Start-PesterSpecTest -Path C:\AppX.Tests.ps1` 36 | 37 | 5. See how well your scripts worked. 38 | 39 | ![alt text](/readme-image.png?raw=true) 40 | -------------------------------------------------------------------------------- /Tests/File.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'File' { 2 | 3 | $commonParams = @{ 4 | $Node = 'TEST123' 5 | $Path = 'C:\Whatever.txt' 6 | } 7 | 8 | mock It { return $null } -ModuleName 'File' 9 | mock Invoke-Command { return [pscustomobject]@{ 'Length' = 1000 } } -ModuleName 'File' 10 | 11 | it 'queries the right node' { 12 | File @commonParams -Exists 13 | Assert-MockCalled Invoke-Command -ParameterFilter { $ComputerName -eq $Node } -ModuleName 'File' 14 | } 15 | 16 | it 'queries the right file path' { 17 | File @commonParams -Exists 18 | Assert-MockCalled Invoke-Command -ParameterFilter { [string]$ScriptBlock -like '*Get-Item -Path $using:Path -ErrorAction Ignore*' } -ModuleName 'File' 19 | } 20 | 21 | it 'Parameter sets are correct' { 22 | { File @commonParams -Exists -SizeInBytes 1000 } | Should throw 'Parameter set cannot be resolved using the specified named parameters.' 23 | } 24 | 25 | it 'Cannot pass empty values to parameters' { 26 | $err = 'The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.' 27 | { File -Node $null -Path $Path -Exists } | Should throw $err 28 | { File -Node $Node -Path $null -Exists } | Should throw $err 29 | # { File @commonParams -SizeInBytes $null } | Should throw $err 30 | # { File @commonParams -SizeInBytes 1000 -Credential $null } | Should throw $err 31 | } 32 | 33 | it 'Cannot pass empty path' { 34 | $err = "Cannot validate argument on parameter 'Path'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again." 35 | { File -Node $Node -Path $null -Exists -SizeInBytes 1000 } | Should throw $err 36 | } 37 | 38 | it 'Mandatory parameters are set correctly' { 39 | { File -Node $Node -Exists } #| Should throw 'Parameter set cannot be resolved using the specified named parameters.' 40 | { File -Path $Path -Exists } #| Should throw 'Parameter set cannot be resolved using the specified named parameters.' 41 | } 42 | 43 | it 'Credential is used with Invoke-Command when passed' { 44 | $secpasswd = ConvertTo-SecureString 'PlainTextPassword' -AsPlainText -Force 45 | $testCred = New-Object System.Management.Automation.PSCredential ('username', $secpasswd) 46 | File @commonParams -Credential $testCred -Exists 47 | Assert-MockCalled Invoke-Command -ParameterFilter { $Credential.UserName -eq $testCred.Username } -ModuleName 'File' 48 | 49 | } 50 | 51 | it 'all parameters have appropriate types' { 52 | { File @commonParams -Exists -SizeInBytes 'stringhere' } | Should throw 'Cannot convert value "stringhere" to type "System.Int32"' 53 | { File -Node @() -Path $Path -Exists } | Should throw "Cannot process argument transformation on parameter 'Node'. Cannot convert value to type System.String." 54 | { File -Node $Node -Path @() -Exists } | Should throw "Cannot process argument transformation on parameter 'Path'. Cannot convert value to type System.String." 55 | { File @commonParams -Exists $true } | Should throw "A positional parameter cannot be found that accepts argument 'True'" 56 | { File @commonParams -Exists -Credential @() } | Should throw "Cannot process argument transformation on parameter 'Credential'. userName" 57 | 58 | } 59 | } -------------------------------------------------------------------------------- /Tests/Service.Tests.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/PesterSpec/28951e4b777c6959b322f7fb98b5e4cfa5321c75/Tests/Service.Tests.ps1 -------------------------------------------------------------------------------- /readme-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/PesterSpec/28951e4b777c6959b322f7fb98b5e4cfa5321c75/readme-image.png --------------------------------------------------------------------------------