├── .gitattributes ├── Infrastructure Validation ├── Invoke-AdSync.Tests.ps1 └── Invoke-AdSync.ps1 ├── Invoke-Parallel-master.zip ├── README.md ├── VSCodePesterSnippets.json ├── azure-pipelines.yml └── improving-code-coverage ├── Get-MachineInfo-complete.Tests.ps1 ├── Get-MachineInfo-start.Tests.ps1 └── get-machineinfo.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Infrastructure Validation/Invoke-AdSync.Tests.ps1: -------------------------------------------------------------------------------- 1 | describe 'AD CSV Sync script' { 2 | 3 | $employeeCsvFileLocation = 'C:\TestArtifacts\Employees.csv' 4 | 5 | $dependencies = @( 6 | @{ 7 | Label = "CSV file at $employeeCsvLocation exists" 8 | Test = { Test-Path -Path $employeeCsvLocation } 9 | } 10 | @{ 11 | Label = "The $(whoami) user can read AD user objects" 12 | Test = { [bool](Get-AdUser -Identity 'S-1-5-21-4117810001-3432493942-696130396-500') } 13 | } 14 | ) 15 | 16 | foreach ($dep in $dependencies) { 17 | if (-not (& $dep)) { 18 | throw "The check: $($dep.Label) failed. Halting all tests.' 19 | } 20 | } 21 | 22 | & C:\Scripts\Invoke-Adsync.ps1 23 | 24 | it 'when provided a valid CSV file, it creates a user account for each employee inside the CSV file' { 25 | 26 | ## This should be covered by a unit test assuming the CSV has all the right properties 27 | $employees = Import-Csv -Path $employeeCsvFileLocation 28 | 29 | foreach ($employee in $employees) { 30 | $adUserParams = @{ 31 | Filter = "givenName -eq '$($employee.FirstName)' -and surName -eq '$($employee.LastName)'" 32 | Properties = 'Department','Title' 33 | } 34 | $user = Get-ADUser @adUserParams 35 | $user | should not benullorempty 36 | $user.Department | should be $employee.Department 37 | $user.Title | should be $employee.Title 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /Infrastructure Validation/Invoke-AdSync.ps1: -------------------------------------------------------------------------------- 1 | ## Grab the employee names from a CSV file 2 | $employees = Import-Csv -Path C:\employees.csv 3 | 4 | foreach ($employee in $employees) { 5 | ## Check to see if the user account already exists 6 | $adUserParams = @{ 7 | Filter = "givenName -eq '$($employee.FirstName)' -and surName -eq '$($employee.LastName)'" 8 | } 9 | if (-not (Get-ADUser @adUserParams)) { 10 | ## If not, create a new user account 11 | $newAdUserParams = @{ 12 | Name = "$($employee.FirstName)$($employee.LastName)" 13 | GivenName = $employee.FirstName 14 | SurName = $employee.LastName 15 | Department = $employee.Department 16 | Title = $employee.Title 17 | } 18 | New-ADUser @newAdUserParams 19 | } 20 | } -------------------------------------------------------------------------------- /Invoke-Parallel-master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/pesterbookcode/0303b3b870c7c408f04eba795615e7f88002216b/Invoke-Parallel-master.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Pester Book 2 | This repo consists of open-source code samples from _The Pester Book_ by Adam Bertram. 3 | -------------------------------------------------------------------------------- /VSCodePesterSnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Pester - Script Analyzer Test": { 3 | "prefix": "psat", 4 | "body": [ 5 | "it 'should pass all error-level script analyzer rules' {", 6 | "", 7 | "\t$excludedRules = @(", 8 | "\t", 9 | "\t)", 10 | "", 11 | "\tInvoke-ScriptAnalyzer -Path $PSScriptRoot -ExcludeRule $excludedRules -Severity Error | should benullorempty", 12 | "}" 13 | ], 14 | "description": "Pester - Script Analyzer Test" 15 | }, 16 | "Set-TestInconclusive": { 17 | "prefix": "psti", 18 | "body": [ 19 | "Set-TestInconclusive -Message '${message}'" 20 | ], 21 | "description": "Set-TestInconclusive" 22 | }, 23 | "Pester - Mocked Credential": { 24 | "prefix": "pcredmock", 25 | "body": [ 26 | "$$cred = New-MockObject -Type 'System.Management.Automation.PSCredential'", 27 | "$$cred | Add-Member -MemberType ScriptMethod -Name 'GetNetworkCredential' -Force -Value { [pscustomobject]@{Password = 'pwhere'} }", 28 | "$$cred | Add-Member -MemberType NoteProperty -Name 'UserName' -Force -Value 'user@domain.local' -PassThru" 29 | ], 30 | "description": "Pester - Mocked Credential" 31 | }, 32 | "Pester - mock null": { 33 | "prefix": "mockn", 34 | "body": [ 35 | "mock '${$$command}'$1" 36 | ], 37 | "description": "Pester - mock null" 38 | }, 39 | "Pester - mock $true": { 40 | "prefix": "mocktrue", 41 | "body": [ 42 | "mock '${$$command}'$1 {", 43 | "\t$$true", 44 | "}" 45 | ], 46 | "description": "Pester - mock $true" 47 | }, 48 | "Pester - mock $false": { 49 | "prefix": "mockfalse", 50 | "body": [ 51 | "mock '${$$command}'$1 {", 52 | "\t$$false", 53 | "}" 54 | ], 55 | "description": "Pester - mock $false" 56 | }, 57 | "Pester - Should Be": { 58 | "prefix": "psb", 59 | "body": [ 60 | "should be" 61 | ], 62 | "description": "Pester - Should Be" 63 | }, 64 | "Pester - Assert-MockCalled ParameterFilter": { 65 | "prefix": "ppf", 66 | "body": [ 67 | "-not (diff ([array]$$PSBoundParameters.Keys) @('$1')) -and", 68 | "-not (diff ([array]$$PSBoundParameters.Values) @('$2'))" 69 | ], 70 | "description": "Pester - Assert-MockCalled ParameterFilter" 71 | }, 72 | "Pester - New Script File for Module": { 73 | "prefix": "npscript", 74 | "body": [ 75 | "#region import modules", 76 | "$$ThisModule = \"$($$MyInvocation.MyCommand.Path -replace '\\.Tests\\.ps1$', '').psm1\"", 77 | "$$ThisModuleName = (($$ThisModule | Split-Path -Leaf) -replace '\\.psm1')", 78 | "Get-Module -Name $$ThisModuleName -All | Remove-Module -Force", 79 | "", 80 | "Import-Module -Name $$ThisModule -Force -ErrorAction Stop", 81 | "", 82 | "@(Get-Module -Name $$ThisModuleName).where({ $$_.version -ne '0.0' }) | Remove-Module", 83 | "#endregion", 84 | "", 85 | "InModuleScope $$ThisModuleName {", 86 | "\t", 87 | "}" 88 | ], 89 | "description": "Pester - New Script File" 90 | }, 91 | "Pester - BeforeAll": { 92 | "prefix": "pba", 93 | "body": [ 94 | "BeforeAll {", 95 | "\t", 96 | "}" 97 | ], 98 | "description": "Pester - BeforeAll" 99 | }, 100 | "Pester - AfterAll": { 101 | "prefix": "paa", 102 | "body": [ 103 | "AfterAll {", 104 | "\t", 105 | "}" 106 | ], 107 | "description": "Pester - AfterAll" 108 | }, 109 | "Pester - BeforeEach": { 110 | "prefix": "pbe", 111 | "body": [ 112 | "BeforeEach {", 113 | "\t", 114 | "}" 115 | ], 116 | "description": "Pester - BeforeEach" 117 | }, 118 | "Pester - AfterEach": { 119 | "prefix": "pae", 120 | "body": [ 121 | "AfterEach {", 122 | "\t", 123 | "}" 124 | ], 125 | "description": "Pester - AfterEach" 126 | }, 127 | "Pester - AssertVerifiableMocks": { 128 | "prefix": "pavm", 129 | "body": [ 130 | "Assert-VerifiableMocks" 131 | ], 132 | "description": "Pester - AssertVerifiableMocks" 133 | }, 134 | "Pester - InModuleScope": { 135 | "prefix": "pimc", 136 | "body": [ 137 | "InModuleScope '${moduleName}' {", 138 | "\t", 139 | "}" 140 | ], 141 | "description": "Pester - InModuleScope" 142 | }, 143 | "Pester - Describe block (Unit Test)": { 144 | "prefix": "descu", 145 | "body": [ 146 | "describe '${name}' {", 147 | "", 148 | "\t$$commandName = '${name}'", 149 | "\t$$command = Get-Command -Name $$commandName", 150 | "", 151 | "\t#region Mocks", 152 | "\t#endregion", 153 | "\t", 154 | "\t$$parameterSets = @(", 155 | "\t\t@{", 156 | "\t\t\tTestName = ''", 157 | "\t\t}", 158 | "\t)", 159 | "", 160 | "\t$$testCases = @{", 161 | "\t\tAll = $$parameterSets", 162 | "\t}", 163 | "", 164 | "\tit 'returns the same object type as defined in OutputType: ' -TestCases $$testCases.All {", 165 | "\t\tparam()", 166 | "", 167 | "\t\t& $$commandName @PSBoundParameters | should beoftype $$command.OutputType.Name", 168 | "", 169 | "\t}", 170 | "}" 171 | ], 172 | "description": "Pester - Describe block" 173 | }, 174 | "Pester - Help Tests": { 175 | "prefix": "phelp", 176 | "body": [ 177 | "\tcontext 'Help' {", 178 | "\t\t", 179 | "\t\t$$nativeParamNames = @(", 180 | "\t\t\t'Verbose'", 181 | "\t\t\t'Debug'", 182 | "\t\t\t'ErrorAction'", 183 | "\t\t\t'WarningAction'", 184 | "\t\t\t'InformationAction'", 185 | "\t\t\t'ErrorVariable'", 186 | "\t\t\t'WarningVariable'", 187 | "\t\t\t'InformationVariable'", 188 | "\t\t\t'OutVariable'", 189 | "\t\t\t'OutBuffer'", 190 | "\t\t\t'PipelineVariable'", 191 | "\t\t\t'Confirm'", 192 | "\t\t\t'WhatIf'", 193 | "\t\t)", 194 | "\t\t", 195 | "\t\t$$command = Get-Command -Name $$commandName", 196 | "\t\t$$commandParamNames = [array]($$command.Parameters.Keys | where {$$_ -notin $$nativeParamNames})", 197 | "\t\t$$help = Get-Help -Name $$commandName", 198 | "\t\t$$helpParamNames = $$help.parameters.parameter.name", 199 | "\t\t", 200 | "\t\tit 'has a SYNOPSIS defined' {", 201 | "\t\t\t$$help.synopsis | should not match $$commandName", 202 | "\t\t}", 203 | "\t\t", 204 | "\t\tit 'has at least one example' {", 205 | "\t\t\t$$help.examples | should not benullorempty", 206 | "\t\t}", 207 | "\t\t", 208 | "\t\tit 'all help parameters have a description' {", 209 | "\t\t\t$$help.Parameters | where { ('Description' -in $$_.Parameter.PSObject.Properties.Name) -and (-not $$_.Parameter.Description) } | should be $$null", 210 | "\t\t}", 211 | "\t\t", 212 | "\t\tit 'there are no help parameters that refer to non-existent command paramaters' {", 213 | "\t\t\tif ($$commandParamNames) {", 214 | "\t\t\t@(Compare-Object -ReferenceObject $$helpParamNames -DifferenceObject $$commandParamNames).where({", 215 | "\t\t\t\t$$_.SideIndicator -eq '<='", 216 | "\t\t\t}) | should benullorempty", 217 | "\t\t\t}", 218 | "\t\t}", 219 | "\t\t", 220 | "\t\tit 'all command parameters have a help parameter defined' {", 221 | "\t\t\tif ($$commandParamNames) {", 222 | "\t\t\t@(Compare-Object -ReferenceObject $$helpParamNames -DifferenceObject $$commandParamNames).where({", 223 | "\t\t\t\t$$_.SideIndicator -eq '=>'", 224 | "\t\t\t}) | should benullorempty", 225 | "\t\t\t}", 226 | "\t\t}", 227 | "\t}" 228 | ], 229 | "description": "Pester - Help Tests" 230 | }, 231 | "Pester - Describe block (Functional Test)": { 232 | "prefix": "descf", 233 | "body": [ 234 | "describe '${name}' {", 235 | "", 236 | "\t$$commandName = '${name}'", 237 | "\t$$command = Get-Command -Name $$commandName", 238 | "", 239 | "\t$$parameterSets = @(", 240 | "\t\t@{", 241 | "\t\t\tTestName = ''", 242 | "\t\t}", 243 | "\t)", 244 | "", 245 | "\t$$testCases = @{", 246 | "\t\tAll = $$parameterSets", 247 | "\t}", 248 | "", 249 | "\tit 'should not throw an exception: ' -TestCases $$testCases.All {", 250 | "\t\tparam($1)", 251 | "", 252 | "\t\t$$params = @{} + $$PSBoundParameters", 253 | "\t\t{ & $$commandName @params } | should not throw ", 254 | "\t}", 255 | "}" 256 | ], 257 | "description": "Pester - Describe block" 258 | }, 259 | "Pester - Describe block (Unit Test, Helper Function)": { 260 | "prefix": "descuh", 261 | "body": [ 262 | "describe '${name}' {", 263 | "", 264 | "\t$$commandName = '${name}'", 265 | "\t$$command = Get-Command -Name $$commandName", 266 | "", 267 | "\t$$parameterSets = @(", 268 | "\t\t@{", 269 | "\t\t\t", 270 | "\t\t}", 271 | "\t)", 272 | "", 273 | "\t$$testCases = @{", 274 | "\t\tAll = $$parameterSets", 275 | "\t}", 276 | "", 277 | "\tit 'returns the same object type as defined in OutputType' -TestCases $$testCases.All {", 278 | "\t\tparam()", 279 | "\t\t& $$commandName @PSBoundParameters | should beoftype $$command.OutputType.Name", 280 | "\t\t}", 281 | "}" 282 | ], 283 | "description": "Pester - Describe block" 284 | }, 285 | "Pester - New-MockObject": { 286 | "prefix": "nmo", 287 | "body": [ 288 | "New-MockObject -Type '$1'" 289 | ], 290 | "description": "New-MockObject" 291 | }, 292 | "Pester - Test Case": { 293 | "prefix": "ptc", 294 | "body": [ 295 | "${Name} = $$parameterSets.where({$$_.ContainsKey('$1')})" 296 | ], 297 | "description": "Pester Test Case" 298 | }, 299 | "Pester - Assert-MockCalled": { 300 | "prefix": "assm", 301 | "body": [ 302 | "$$assMParams = @{", 303 | "\tCommandName = '${command}'", 304 | "\tTimes = 1", 305 | "\tExactly = $$true", 306 | "\tScope = 'It'", 307 | "\tParameterFilter = { $$PSBoundParameters.${paramName} -eq ${paramValue} }", 308 | "}", 309 | "Assert-MockCalled @assMParams" 310 | ], 311 | "description": "Assert-MockCalled" 312 | }, 313 | "Pester - It block": { 314 | "prefix": "it", 315 | "body": [ 316 | "it 'should ${name}: ' -TestCases $$testCases.${All} {", 317 | "\tparam($1)", 318 | "", 319 | "\t$$result = & $$commandName @PSBoundParameters", 320 | "}" 321 | ], 322 | "description": "Pester - It block" 323 | }, 324 | "Pester - It block (Throws Exception)": { 325 | "prefix": "itexc", 326 | "body": [ 327 | "it 'should throw an exception: ' -TestCases $$testCases.All {", 328 | "\tparam($1)", 329 | "", 330 | "\t$$params = @{} + $$PSBoundParameters", 331 | "\t{ & $$commandName @params } | should throw ", 332 | "}" 333 | ], 334 | "description": "Pester - It block" 335 | }, 336 | "Pester - It block (Assertion)": { 337 | "prefix": "itass", 338 | "body": [ 339 | "it 'should pass the expected parameters to ${name}: ' -TestCases $$testCases.All {", 340 | "\tparam($1)", 341 | "", 342 | "\t$$null = & $$commandName @PSBoundParameters", 343 | "", 344 | "\t$$assMParams = @{", 345 | "\t\tCommandName = '${name}'", 346 | "\t\tTimes = 1", 347 | "\t\tExactly = $$true", 348 | "\t\tScope = 'It'", 349 | "\t\tParameterFilter = { $2 }", 350 | "\t}", 351 | "\tAssert-MockCalled @assMParams", 352 | "}" 353 | ], 354 | "description": "Pester - It block" 355 | }, 356 | "Pester - It block exception thrown": { 357 | "prefix": "itexception", 358 | "body": [ 359 | "" 360 | ], 361 | "description": "Pester - It block exception thrown" 362 | }, 363 | "Pester - Context Block": { 364 | "prefix": "conte", 365 | "body": [ 366 | "context 'when $1' {", 367 | "", 368 | "\t$2", 369 | "", 370 | "}" 371 | ], 372 | "description": "Pester - Context Block" 373 | } 374 | } -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | global_variable: value # this is a global available to all stages and jobs 3 | 4 | stages: 5 | - stage: Build 6 | variables: 7 | stage_variable1: value3 # available in Build stage and all jobs 8 | jobs: 9 | - job: BuildJob 10 | variables: 11 | job_variable1: value1 # this is only available in BuildJob 12 | steps: 13 | - bash: echo $(stage_variable1) ## works 14 | - bash: echo $(global_variable) ## works 15 | - bash: echo $(job_variable1) ## works -------------------------------------------------------------------------------- /improving-code-coverage/Get-MachineInfo-complete.Tests.ps1: -------------------------------------------------------------------------------- 1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 2 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '.Tests.', '.' 3 | . "$here\$sut" 4 | 5 | describe 'Get-MachineInfo' { 6 | mock 'New-CimSessionOption' { 7 | New-MockObject -Type 'Microsoft.Management.Infrastructure.Options.WSManSessionOptions' 8 | } 9 | 10 | mock 'New-CimSession' { 11 | New-MockObject -Type 'Microsoft.Management.Infrastructure.CimSession' 12 | } 13 | 14 | mock 'Get-CimInstance' { 15 | @{ 16 | Manufacturer = 'manhere' 17 | Model = 'modelhere' 18 | NumberOfProcessors = 'numberofprocshere' 19 | NumberOfLogicalProcessors = 'numlogprocshere' 20 | TotalPhysicalMemory = '1000' 21 | } 22 | } -ParameterFilter { $PSBoundParameters.ClassName -eq 'Win32_ComputerSystem' } 23 | 24 | mock 'Get-CimInstance' { 25 | @{ 26 | Freespace = 'freespacehere' 27 | SystemDrive = 'C:' 28 | Version = 'ver' 29 | ServicePackMajorVersion = 'spver' 30 | BuildNumber = 'buildnum' 31 | } 32 | } -ParameterFilter { $PSBoundParameters.ClassName -eq 'Win32_OperatingSystem' } 33 | 34 | mock 'Get-CimInstance' { 35 | @{ 36 | Freespace = 'freespacehere' 37 | } 38 | } -ParameterFilter { $PSBoundParameters.ClassName -eq 'Win32_LogicalDisk' } 39 | 40 | mock 'Get-CimInstance' { 41 | @{ AddressWidth = 'Addrwidth' } 42 | @{ AddressWidth = 'Addrwidth2' } 43 | } -ParameterFilter { $PSBoundParameters.ClassName -eq 'Win32_Processor' } 44 | 45 | mock 'Remove-CimSession' 46 | mock 'Out-File' 47 | mock 'Write-Verbose' 48 | 49 | it 'allows multiple computer names to be passed to it' { 50 | { $null = Get-MachineInfo -ComputerName FOO, FOO1, FOO2 } | should not throw 51 | } 52 | 53 | it 'only allows the strings "Wsman" and "Dcom" to be used for the Protocol parameter' { 54 | { Get-MachineInfo -ComputerName FOO -Protocol 1 } | should throw 55 | } 56 | 57 | it 'should create a DCOM CIM session when the DCOM protocol is specified' { 58 | $result = Get-MachineInfo -ComputerName FOO -Protocol Dcom 59 | $assMParams = @{ 60 | CommandName = 'New-CimSessionOption' 61 | Times = 1 62 | Exactly = $true 63 | Scope = 'It' 64 | ParameterFilter = { $PSBoundParameters.Protocol -eq 'Dcom' } 65 | } 66 | Assert-MockCalled @assMParams 67 | } 68 | 69 | it 'should create a WSMAN CIM session when the WSMAN protocol is specified' { 70 | $result = Get-MachineInfo -ComputerName FOO -Protocol Wsman 71 | $assMParams = @{ 72 | CommandName = 'New-CimSessionOption' 73 | Times = 1 74 | Exactly = $true 75 | Scope = 'It' 76 | ParameterFilter = { $PSBoundParameters.Protocol -eq 'Wsman' } 77 | 78 | } 79 | Assert-MockCalled @assMParams 80 | } 81 | 82 | it 'should create a CIM session for each computer provided' { 83 | $computers = 'FOO1', 'FOO2' 84 | $result = Get-MachineInfo -ComputerName $computers 85 | foreach ($comp in $computers) { 86 | $assMParams = @{ 87 | CommandName = 'New-CimSession' 88 | Times = 1 89 | Exactly = $true 90 | Scope = 'It' 91 | ParameterFilter = { $PSBoundParameters.ComputerName -eq $comp } 92 | } 93 | Assert-MockCalled @assMParams 94 | } 95 | } 96 | 97 | it 'should query the Win32_ComputerSystem CIM class on each computer provided' { 98 | $computers = 'FOO1', 'FOO2' 99 | $result = Get-MachineInfo -ComputerName $computers 100 | $assMParams = @{ 101 | CommandName = 'Get-CimInstance' 102 | Times = @($computers).Count 103 | Exactly = $true 104 | Scope = 'It' 105 | ParameterFilter = { $PSBoundParameters.ClassName -eq 'Win32_ComputerSystem' } 106 | } 107 | Assert-MockCalled @assMParams 108 | } 109 | 110 | it 'should query the Win32_OperatingSystem CIM class on each computer provided' { 111 | $computers = 'FOO1', 'FOO2' 112 | $result = Get-MachineInfo -ComputerName $computers 113 | $assMParams = @{ 114 | CommandName = 'Get-CimInstance' 115 | Times = @($computers).Count 116 | Exactly = $true 117 | Scope = 'It' 118 | ParameterFilter = { $PSBoundParameters.ClassName -eq 'Win32_OperatingSystem' } 119 | } 120 | 121 | Assert-MockCalled @assMParams 122 | } 123 | 124 | it 'should query the Win32_LogicalDisk CIM class on each computer provided' { 125 | $computers = 'FOO1', 'FOO2' 126 | $result = Get-MachineInfo -ComputerName $computers 127 | $assMParams = @{ 128 | CommandName = 'Get-CimInstance' 129 | Times = @($computers).Count 130 | Exactly = $true 131 | Scope = 'It' 132 | ParameterFilter = { $PSBoundParameters.ClassName -eq 'Win32_LogicalDisk' } 133 | } 134 | 135 | Assert-MockCalled @assMParams 136 | } 137 | 138 | it 'should query the Win32_Processor CIM class on each computer provided' { 139 | $computers = 'FOO1', 'FOO2' 140 | $result = Get-MachineInfo -ComputerName $computers 141 | $assMParams = @{ 142 | CommandName = 'Get-CimInstance' 143 | Times = @($computers).Count 144 | Exactly = $true 145 | Scope = 'It' 146 | ParameterFilter = { $PSBoundParameters.ClassName -eq 'Win32_Processor' } 147 | } 148 | Assert-MockCalled @assMParams 149 | } 150 | 151 | it 'should only return the first instance of the Win32_Processor CIM class on each computer provided' { 152 | $computers = 'FOO1', 'FOO2' 153 | $result = Get-MachineInfo -ComputerName $computers 154 | $result[0].Arch | should be 'Addrwidth' 155 | $result[1].Arch | should be 'Addrwidth' 156 | } 157 | 158 | context 'When the function throws an exception' { 159 | 160 | mock 'New-CimSession' { throw } 161 | 162 | context 'when an exception is thrown when querying a computer, and ProtocolFallBack is used, and WSMAN is used as the Protocol' { 163 | 164 | mock 'Get-MachineInfo' { } -ParameterFilter { $Protocol -eq 'DCOM' } 165 | 166 | it 'when an exception is thrown when querying a computer, and ProtocolFallBack and LogFailuresToPath are used, it should call itself using the DCOM protocol' { 167 | 168 | $result = Get-MachineInfo -ComputerName FOO -Protocol WSMAN -ProtocolFallBack 169 | 170 | $assMParams = @{ 171 | CommandName = 'Get-MachineInfo' 172 | Times = 1 173 | Exactly = $true 174 | Scope = 'It' 175 | } 176 | Assert-MockCalled @assMParams 177 | 178 | } 179 | } 180 | 181 | context 'when an exception is thrown when querying a computer, and ProtocolFallBack is used, and DCOM is used as the Protocol' { 182 | 183 | mock 'Get-MachineInfo' { } -ParameterFilter { $Protocol -eq 'WSMAN' } 184 | 185 | it 'when an exception is thrown when querying a computer, and ProtocolFallBack is used, and DCOM is used as the Protocol, it should call itself using the WSMAN protocol' { 186 | 187 | $result = Get-MachineInfo -ComputerName FOO -Protocol DCOM -ProtocolFallBack 188 | 189 | $assMParams = @{ 190 | CommandName = 'Get-MachineInfo' 191 | Times = 1 192 | Exactly = $true 193 | Scope = 'It' 194 | } 195 | Assert-MockCalled @assMParams 196 | } 197 | } 198 | 199 | context 'when an exception is thrown when querying a computer, and ProtocolFallBack and LogFailuresToPath are used' { 200 | 201 | mock 'Get-MachineInfo' { } -ParameterFilter { $LogFailuresToPath -eq 'C:\Path' } 202 | 203 | it 'when an exception is thrown when querying a computer, and ProtocolFallBack and LogFailuresToPath are used, it should call itself using the LogFailuresToPath parameter' { 204 | 205 | $result = Get-MachineInfo -ComputerName FOO -ProtocolFallBack -LogFailuresToPath 'C:\Path' 206 | 207 | $assMParams = @{ 208 | CommandName = 'Get-MachineInfo' 209 | Times = 1 210 | Exactly = $true 211 | Scope = 'It' 212 | } 213 | Assert-MockCalled @assMParams 214 | } 215 | } 216 | 217 | it 'when an exception is thrown when querying a computer, and ProtocolFallBack is not used, and LogFailuresToPath is used, it writes the computer name to a file' { 218 | $result = Get-MachineInfo -ComputerName FOO -LogFailuresToPath 'C:\Path' 219 | $assMParams = @{ 220 | CommandName = 'Out-File' 221 | Times = 1 222 | Exactly = $true 223 | Scope = 'It' 224 | ParameterFilter = { 225 | $PSBoundParameters.FilePath -eq 'C:\Path' -and 226 | $PSBoundParameters.InputObject -eq 'FOO' 227 | } 228 | } 229 | Assert-MockCalled @assMParams 230 | } 231 | } 232 | 233 | it 'should return a single pscustomobject for each computer provided' { 234 | $computers = 'FOO1', 'FOO2' 235 | $result = Get-MachineInfo -ComputerName $computers 236 | @($result).Count | should be @($computers).Count 237 | } 238 | 239 | it 'should return a pscustomobject with expected property names for each computer provided' { 240 | $computers = 'FOO1', 'FOO2' 241 | $result = Get-MachineInfo -ComputerName $computers 242 | foreach ($obj in $result) { 243 | $obj.OSVersion | should be 'ver' 244 | $obj.SPVersion | should be 'spver' 245 | $obj.OSBuild | should be 'buildnum' 246 | $obj.Manufacturer | should be 'manhere' 247 | $obj.Model | should be 'modelhere' 248 | $obj.Procs | should be 'numberofprocshere' 249 | $obj.Cores | should be 'numlogprocshere' 250 | $obj.RAM | should be '9.31322574615479E-07' 251 | $obj.Arch | should be 'Addrwidth' 252 | $obj.SysDriveFreeSpace | should be 'freespacehere' 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /improving-code-coverage/Get-MachineInfo-start.Tests.ps1: -------------------------------------------------------------------------------- 1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 2 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '.Tests.', '.' 3 | . "$here\$sut" 4 | 5 | describe 'Get-MachineInfo' { 6 | it 'should return object over CIM' { 7 | $params = @{ 8 | 'ComputerName' ='localhost' 9 | 'Protocol' ='Wsman' 10 | } 11 | 12 | $result = Get-MachineInfo @params 13 | $result.computername | Should Be 'localhost' 14 | } 15 | 16 | it 'should not allow WMI as protocol' { 17 | { Get-MachineInfo -Protocol WMI } | Should Throw 18 | } 19 | 20 | it 'should write error log' { 21 | $params = @{ 22 | 'ComputerName' ='FAIL' 23 | 'LogFailuresToPath' ='TESTDRIVE:\fails.txt' 24 | 'Protocol' ='Wsman' 25 | } 26 | 27 | Get-MachineInfo @params 28 | Get-Content TESTDRIVE:\fails.txt | Should Be 'FAIL' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /improving-code-coverage/get-machineinfo.ps1: -------------------------------------------------------------------------------- 1 | function Get-MachineInfo { 2 | <# 3 | .SYNOPSIS 4 | Retrieves specific information about one or more computers, using WMI or CIM. 5 | 6 | .DESCRIPTION 7 | This command uses either WMI or CIM to retrieve specific information about 8 | one or more computers. You must run this command as a user who has permission 9 | to query CIM or WMI remotely on the machines involved. You can specify a 10 | starting protocol (CIM by default), and specify that, in the event of a 11 | failure, the other protocol be used on a per-machine basis. 12 | 13 | .PARAMETER ComputerName 14 | One or more computer names. When using WMI, this can also be IP addresses. 15 | IP addresses may not work for CIM. 16 | 17 | .PARAMETER LogFailuresToPath 18 | A path and filename to which to write failed computer names. If omitted, 19 | no log will be written. 20 | 21 | .PARAMETER Protocol 22 | Valid values: Wsman (uses CIM) or Dcom (uses WMI). Will be used for all 23 | machines. "Wsman" is the default. 24 | 25 | .PARAMETER ProtocolFallback 26 | Specify this to try the other protocol automatically if a machine fails. 27 | 28 | .EXAMPLE 29 | Get-MachineInfo -ComputerName ONE,TWO,THREE 30 | 31 | This example will query three machines. 32 | 33 | .EXAMPLE 34 | Get-ADComputer -filter * | Select -Expand Name | Get-MachineInfo 35 | 36 | This example will attempt to query all machines in AD. 37 | #> 38 | [CmdletBinding()] 39 | param( 40 | [Parameter(ValueFromPipeline=$True, Mandatory=$True)] 41 | [Alias('CN', 'MachineName', 'Name')] 42 | [string[]]$ComputerName, 43 | 44 | [Parameter()] 45 | [string]$LogFailuresToPath, 46 | 47 | [Parameter()] 48 | [ValidateSet('Wsman', 'Dcom')] 49 | [string]$Protocol = "Wsman", 50 | 51 | [switch]$ProtocolFallback 52 | ) 53 | 54 | BEGIN { } 55 | 56 | PROCESS { 57 | foreach ($computer in $ComputerName) { 58 | if ($Protocol -eq 'Dcom') { 59 | $option = New-CimSessionOption -Protocol Dcom 60 | } else { 61 | $option = New-CimSessionOption -Protocol Wsman 62 | } 63 | 64 | try { 65 | Write-Verbose "Connecting to $computer over $Protocol" 66 | $params = @{'ComputerName' =$computer 67 | 'SessionOption' =$option 68 | 'ErrorAction' ='Stop' 69 | } 70 | 71 | $session = New-CimSession @params 72 | 73 | Write-Verbose "Querying from $computer" 74 | 75 | $os_params = @{ 76 | 'ClassName' ='Win32_OperatingSystem' 77 | 'CimSession' =$session 78 | } 79 | 80 | $os = Get-CimInstance @os_params 81 | 82 | $cs_params = @{ 83 | 'ClassName' ='Win32_ComputerSystem' 84 | 'CimSession' =$session 85 | } 86 | 87 | $cs = Get-CimInstance @cs_params 88 | 89 | $sysdrive = $os.SystemDrive 90 | $drive_params = @{ 91 | 'ClassName' ='Win32_LogicalDisk' 92 | 'Filter' ="DeviceId='$sysdrive'" 93 | 'CimSession' =$session 94 | } 95 | 96 | $drive = Get-CimInstance @drive_params 97 | 98 | $proc_params = @{ 99 | 'ClassName' ='Win32_Processor' 100 | 'CimSession' =$session 101 | } 102 | 103 | $proc = Get-CimInstance @proc_params | Select-Object -first 1 104 | 105 | Write-Verbose "Closing session to $computer" 106 | 107 | $session | Remove-CimSession 108 | 109 | Write-Verbose "Outputting for $computer" 110 | $obj = [pscustomobject]@{ 111 | 'ComputerName' =$computer 112 | 'OSVersion' =$os.Version 113 | 'SPVersion' =$os.ServicePackMajorVersion 114 | 'OSBuild' =$os.BuildNumber 115 | 'Manufacturer' =$cs.Manufacturer 116 | 'Model' =$cs.Model 117 | 'Procs' =$cs.NumberOfProcessors 118 | 'Cores' =$cs.NumberOfLogicalProcessors 119 | 'RAM' =($cs.TotalPhysicalMemory / 1GB) 120 | 'Arch' =$proc.AddressWidth 121 | 'SysDriveFreeSpace' =$drive.FreeSpace 122 | } 123 | 124 | Write-Output $obj 125 | 126 | } catch { 127 | 128 | # Did I specify protocol fallback? 129 | # If so, try again. If I specified logging, 130 | # I won't log a problem here; we'll let 131 | # the logging occur if this fallback also 132 | # fails 133 | 134 | if ($ProtocolFallback) { 135 | if ($Protocol -eq 'Dcom') { 136 | $newprotocol = 'Wsman' 137 | } else { 138 | $newprotocol = 'Dcom' 139 | } 140 | 141 | Write-Verbose "Trying again with $newprotocol" 142 | $params = @{ 143 | 'ComputerName' =$Computer 144 | 'Protocol' =$newprotocol 145 | 'ProtocolFallback' =$False 146 | } 147 | 148 | if ($PSBoundParameters.ContainsKey('LogFailuresToPath')){ 149 | $params += @{'LogFailuresToPath' =$LogFailuresToPath } 150 | } 151 | 152 | Get-MachineInfo @params 153 | } 154 | 155 | # if I didn't specify fallback, but we did specify logging, then log the error, 156 | # because I won't try again. 157 | if (-not $ProtocolFallback -and $PSBoundParameters.ContainsKey('LogFailuresToPath')){ 158 | Write-Verbose "Logging to $LogFailuresToPath" 159 | $computer | Out-File $LogFailuresToPath -Append 160 | } 161 | } 162 | } 163 | } 164 | } 165 | --------------------------------------------------------------------------------