I am trying to test a Function in an script file that is testing the connection to a PC. I am trying to do that by calling the mock "Test-Connection" from an other Script in an Pester Test.
When I run Temp.Testing.ps1
describe 'Test Error Handling' {
$myDir = Split-Path -parent $PSCommandPath
$testFile = "$myDir\TryToRenameComputer.ps1"
.$testFile
mock 'Test-Connection' { $false }
$pathForLogs = "C:\temp\Logs.txt"
it 'exits if Test-Connection Failed'{
TryToRenameComputer -OldName "OldName"
Assert-MockCalled 'Test-Connection' -Times 1 -Scope It
}
}
TryToRenameComputer.ps1
function TryToRenameComputer {
param([parameter(Mandatory=$true)]
[string]$computerName)
if (!(Test-Connection -ComputerName $computerName -Quiet)) {
exit
}
}
it skips the it-statement and shows me no errors. Only "Describing Test Error Handling".
Expected Result:
Actual Result:
I already tried running a other Function and it worked.
Also when I am running multiple it-statements all get skipped when I call the Function in 1 it-statement.
I also tried rewriting it so it is no function anymore and it worked.
The problem is likely occurring because of the exit statement in your code. This is getting executed because in the if statement before it you are using -not via its shorthand of ! to test for the result of Test-Connection and because your Mock sets Test-Connection to $false.
By using exit you are immediately terminating the PowerShell host, which is then stopping your tests from executing/completing.
Instead of using exit consider using break or return to stop the execution of the function without terminating the script. Or if you potentially do want to terminate the script, consider using throw as you can then stop the parent script when an exception has occurred.
You could then modify your tests to test for that throw as that is the result you expect to occur when test-connection returns $false. For example:
function TryToRenameComputer {
param([parameter(Mandatory=$true)]
[string]$computerName)
if (!(Test-Connection -ComputerName $computerName -Quiet)) {
Thow "Could not connect to $computerName"
}
}
describe 'Test Error Handling' {
$myDir = Split-Path -parent $PSCommandPath
$testFile = "$myDir\TryToRenameComputer.ps1"
.$testFile
mock 'Test-Connection' { $false }
$pathForLogs = "C:\temp\Logs.txt"
it 'Throws an error if Test-Connection Failed'{
{ TryToRenameComputer -OldName "OldName" } | Should -Throw
Assert-MockCalled 'Test-Connection' -Times 1 -Scope It
}
}
There's no easy way to handle exit in your tests and its generally a bit of an anti-pattern when you're writing code for automation.
Related
I'm looking for method to create a wrapper around Invoke-Command that restores the current directory that I'm using on the remote machine before invoking my command. Here's what I tried to do:
function nice_invoke {
param(
[string]$Computer,
[scriptblock]$ScriptBlock
)
Set-PSDebug -Trace 0
$cwd = (Get-Location).Path
write-host "cmd: $cwd"
$wrapper = {
$target = $using:cwd
if (-not (Test-Path "$target")) {
write-host "ERROR: Directory doesn't exist on remote"
exit 1
}
else {
Set-Location $target
}
$sb = $using:ScriptBlock
$sb.Invoke() | out-host
}
# Execute Command on remote computer in Same Directory as Local Machine
Invoke-Command -Computer pv3039 -ScriptBlock $wrapper
}
Command Line:
PS> nice_invoke -Computer pv3039 -ScriptBlock {get-location |out-host; get-ChildItem | out-host }
Error Message:
Method invocation failed because [System.String]
does not contain a method named 'Invoke'.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
+ PSComputerName : pv3039
You can't pass a ScriptBlock like this with the $using: scope, it will get rendered to a string-literal first. Use the [ScriptBlock]::Create(string) method instead within your $wrapper block to create a ScriptBlock from a String:
$sb = [ScriptBlock]::Create($using:ScriptBlock)
$sb.Invoke() | Out-Host
Alternatively, you could also use Invoke-Command -ArgumentList $ScriptBlock, but you still have the same issue with the ScriptBlock getting rendered as a string. Nonetheless, here is an example for this case as well:
# Call `Invoke-Command -ArgumentList $ScriptBlock`
# $args[0] is the first argument passed into the `Invoke-Command` block
$sb = [ScriptBlock]::Create($args[0])
$sb.Invoke() | Out-Host
Note: While I kept the format here in the way you were attempting to run the ScriptBlock in your original code, the idiomatic way to run ScriptBlocks locally (from the perspective the nested ScriptBlock it is a local execution on the remote machine) is to use the Call Operator like & $sb rather than using $sb.Invoke().
With either approach, the nested ScriptBlock will execute for you from the nested block now. This limitation is similar to how some other types are incompatible with shipping across remote connections or will not survive serialization with Export/Import-CliXml; it is simply a limitation of the ScriptBlock type.
Worthy to note, this limitation persists whether using Invoke-Command or another cmdlet that initiates execution via a child PowerShell session such as Start-Job. So the solution will be the same either way.
function nice_invoke {
param(
[string]$Computer,
[scriptblock]$ScriptBlock
)
Set-PSDebug -Trace 0
$cwd = (Get-Location).Path
write-host "cmd: $cwd"
$wrapper = {
$target = $using:cwd
if (-not (Test-Path "$target")) {
write-host "ERROR: Directory doesn't exist on remote"
exit 1
}
else {
Set-Location $using:cwd
}
$sb = [scriptblock]::Create($using:ScriptBlock)
$sb.Invoke()
}
# Execute Command on remote computer in Same Directory as Local Machine
Invoke-Command -Computer pv3039 -ScriptBlock $wrapper
}
nice_invoke -Computer pv3039 -ScriptBlock {
hostname
get-location
#dir
}
I have a few functions that get called either from Jenkins as part of a pipeline, they also get called from a pester test or lastly they can get called from the powershell console. The issue I have really stems from Jenkins not seeming to handle write-output in the way I think it should.
So what I am doing is creating a Boolean param that will allow my to choose if I terminate my function with a exit code or a return message. The exit code will be used by my pipeline logic and the return message for the rest ?
Is there a alternate approach I should be using this seems to be a bit of a hack.
function Get-ServerPowerState
{
[CmdletBinding()]
param
(
[string[]]$ilo_ip,
[ValidateSet('ON', 'OFF')]
[string]$Status,
[boolean]$fail
)
BEGIN
{
$here = Split-Path -Parent $Script:MyInvocation.MyCommand.Path
$Credentials = IMPORT-CLIXML "$($here)\Lib\iLOCred.xml"
}
PROCESS
{
foreach ($ip in $ilo_ip)
{
New-LogEntry -Message ("Getting current powerstate " + $ip)
If (Test-Connection -ComputerName $ip.ToString() -Count 1 -Quiet)
{
$hostPower = Get-HPiLOhostpower -Server $ip -Credential
$Credentials -DisableCertificateAuthentication
}
}
}
END
{
If($fail){
New-LogEntry -Message "Script been set to fail with exit code" -Log Verbose
New-LogEntry -Message "The host is powered - $($HostPower.Host_Power)" -Log Verbose
If($hostPower.HOST_POWER -match $Status)
{
Exit 0
}
else {
Exit 1
}
}
else {
New-LogEntry -Message "Script been set to NOT fail with exit code" -Log Verbose
New-LogEntry -Message "The host is powered - $($HostPower.Host_Power)" -Log Verbose
If($hostPower.HOST_POWER -match $Status)
{
return 0
}
else {
return 1
}
}
}
}
Like this
function Get-Output {
param ([switch]$asint)
if ($asint) {
return 1
}
else {
write-output 'one'
}
}
Get-Output
Get-Output -asint
If you intend to use the output in the pipeline then use Write-Output. If you intend to only send it to the host process then use Write-Host. I typically use the return keyword if I want to assign a return value to a variable.
[int]$result = Get-Output -asint
How do you exit from an invoke-command script block running on a remote server? I have tried next.
Here is my code:
$Res = Invoke-Command -Session $Ses -ArgumentList ($ROOTdir, $PARAMS.SYS, $PARAMS.main, $PARAMS.zip) -ScriptBlock {
Param($ROOTdir, $SYS, $MAIN, $ZIP)
$list | %{
$completed = $false
$retrycount = 1
while (-not $completed) {
try {
$Copytime = (Measure-Command {
Copy-Item -Path $_.FullName -Destination ($SYS.KITCHENdir) -Force -ErrorVariable copyerror
}).TotalSeconds
$completed = $true
} catch {
if ($retrycount -gt $MAIN.Retry ) {
break or exit #HOW STOP EXECUTING NEXT STEPS AND EXIT
} else {
Start-Sleep $MAIN.DelayRetry
$retrycount++
}
}
}
#IF copy bad result need stop and exit
#The final one can produce flavors here, but it does not look quite kosher
#next steps
}
I suspect you want to use the Exit keyword.
"The exit keyword is used to exit from contexts; it will exit the
(currently running) context where your code is running. This means
that if you use this keyword in a script, and launch the script
directly from your console, it will exit both the script and the
console since they're both running in the same context. However, if
you use the exit keyword in a function within the script, and call
that function from the same script, it will just exit the script and
not the entire console. The exit keyword is best used in functions,
and when those functions are called in a script. It's a great way to
terminate execution of functions."
https://www.pluralsight.com/blog/it-ops/powershell-terminating-code-execution
There's a strange issue in my script.
I'm invoking a command on some servers. If I can't connect to the server (because it's offline or something) I still want to log that (Server is offline > log.txt)
Apparently, when an error occurs in Try block, the Catch block is not executed.
To test this, I wrote a value to the ComputerName parameter that doesn't exist.
Here's my code:
Try {
Invoke-Command -ComputerName hui -ScriptBlock $sb
}
Catch {
Write-Host "Hello"
}
But the Script never puts out "Hello"
The exception I get is a PSRemotingTransportException. I also tried to specify the exception, also didn't work
When I set a breakpoint at the Invoke-Command line, it also ignores the catch block. Why?
It's not a terminating error, so it will never be caught by try/catch. Add -ErrorAction Stop:
Try {
Invoke-Command -ComputerName hui -ScriptBlock $sb -ErrorAction Stop
}
Catch {
Write-Host "Hello"
}
I noticed that if applying a configuration through Start-DscConfiguration fails, it writes to the error stream but doesn't
throw an Exception? That is, if I do the following:
try{
Start-DscConfiguration -Path ".\MyConfig" -Wait -Verbose
}catch{
#...
}
...it never ends up in the catch handler. I suspect this may have something to do with the fact that without the "-Wait",
Start-DscConfiguration starts an async job for this, and async commands probably don't throw exceptions, but in a synchronous
scenario, I would very much like to know if my configuration could be applied.
What is the proper way to determine if Start-DscConfiguration has completed succesfully?
The only way I know is to check the global "$error" variable and compare the number of error records before and after your call to Start-DscConfiguration. If there's more afterwards then something must have gone wrong during the call, so throw your own exception:
Configuration TestErrorHandling {
Node "localhost" {
Script ErroringResource {
GetScript = { return $null; }
TestScript = { return $false; }
SetScript = { throw new-object System.InvalidOperationException; }
}
}
}
$errorCount = $error.Count;
write-host "starting dsc configuration"
$mof = TestErrorHandling;
Start-DscConfiguration TestErrorHandling –Wait –Verbose;
write-host "dsc configuration finished"
if( $error.Count -gt $errorCount )
{
$dscErrors = $error[$errorCount..($error.Count - 1)];
write-host "the following errors occurred during dsc configuration";
write-host ($dscErrors | fl * | out-string);
throw $dscErrors[-1];
}
There's another way to make it cause an exception. Try saving it into the ErrorVariable like this :
try
{
Start-DscConfiguration -Path ".\MyConfig" -Wait -Verbose -ErrorVariable ev
}
catch
{
$myException = $_
}
Weirdly so, this throws the exception when there's an error (which is what you wanted). You can get the value of your exception in the $myexception variable, and also could get just a one liner description of your error using $ev
PS: Note that while mentioning ev in the errorVariable parameter, you do it without the '$' symbol - since you're only specifying the variable 'name'.
Start-DscConfiguration when used without -Wait will create a job object - with one child job for every computername. PowerShell job objects have an Error stream which contains all the errors. You can check this stream as well
$job = Start-DscConfiguration -Force -Verbose -Path C:\Temp\Demo\ -ComputerName localhost
Receive-Job $job -Wait
'Errors in job = ' + ($job.childjobs[0].Error.Count)