Mocking Missing Cmdlet Pipelines with Pester

Following on from the previous the Mocking Missing Cmdlets with Pester and Mocking Missing Cmdlet ErrorAction with Pester posts, I also encountered (yet) another interesting problem when attempting to mock cmdlets that were not present on the test system. This one is pretty similar to the -ErrorAction edge-case but involves the pipeline. Here’s a pseudo test that was failing:

 
Describe 'Mocking Missing Cmdlet Pipeline Example' {
    Function Get-VM { [CmdletBinding()] param ($Name) }
    Function Remove-VM { [CmdletBinding()] param ($Name) }
    
    InModuleScope 'xVMHyper-V' {
 
        It 'Calls Remove-VM' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { return 'TestVM' }
            Mock Remove-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { }
            Get-VM -Name 'TestVM' | Remove-VM;
            Assert-MockCalled Remove-VM -Scope It;
        }
    }
}

In the above example, we wish to mock both the Get-VM and Remove-VM cmdlets and assert that the Remove-VM cmdlet is called. If we run this on a system that does not have the Hyper-V cmdlets installed we receive the following error:

Executing all tests in 'C:\Users\Iain\Desktop\TestPipeline.Tests.ps1'
Describing Mocking Pipeline Example
Remove-VM : The input object cannot be bound to any parameters for the command either because the command does not
take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
At C:\Users\Iain\Desktop\TestPipeline.Tests.ps1:11 char:37
+             Get-VM -Name 'TestVM' | Remove-VM;
+                                     ~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (TestVM:String) [Remove-VM], ParameterBindingException
    + FullyQualifiedErrorId : InputObjectNotBound,Remove-VM

 [-] Calls Remove-VM 1.35s
   Expected Remove-VM to be called at least 1 times but was called 0 times
   at line: 518 in C:\Users\Iain\OneDrive\Powershell\Modules\Pester\Functions\Mock.ps1
Tests completed in 1.35s
Passed: 0 Failed: 1 Skipped: 0 Pending: 0

The error message is fairly self-explanatory. Just like your standard Advanced Function definition, we need to indicate that a parameter needs to be able to accept input via the pipeline using the ValueFromPipeline attribute. To fix this we just need to add the Parameter attribute to our stub function definition:

 
Describe 'Mocking Pipeline Example' {
    Function Get-VM { [CmdletBinding()] param ($Name) }
    Function Remove-VM { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $Name) }
    
    InModuleScope 'xVMHyper-V' {
 
        It 'Calls Remove-VM' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { return 'TestVM' }
            Mock Remove-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { }
            Get-VM -Name 'TestVM' | Remove-VM;
            Assert-MockCalled Remove-VM -Scope It;
        }
    }
}

Whilst it’s not rocket-science – just in case someone else runs into it – I thought it would be worth quickly documenting! Here’s the working example:

Executing all tests in 'C:\Users\Iain\Desktop\TestPipeline.Tests.ps1'
Describing Mocking Pipeline Example
 [+] Calls Remove-VM 74ms
Tests completed in 74ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

 

Configuring CredSSP for Deploying XenDesktop via DSC

During my presentation at E2EVC in Berlin, we released new open source Powershell Desired State Configuration (DSC) resources for Citrix XenDesktop 7. Unfortunately for everyone there, I never actually finished the presentation.

The number of questions and level of interaction in the session was awesome. Even with the attempted sabotage by @drtritsch when he tried to blow the camera up, we recovered and soldiered on. Ultimately – having not completed the presentation and missing a few key implementation details – I thought it would be prudent to at least document how you can use the new resources! You know me – always a true professional :).

Credential Delegation

If you are using the new Citrix XenDesktop DSC resources in conjunction with WMF 4.0 then you will need to configure CredSSP on any machines that you will be installing the Citrix XenDesktop delivery controller role on. These machines will need to be configured to delegate credentials to all other delivery controllers and the Microsoft SQL server machine hosting the XenDesktop databases.

The reason for this is that the underlying Citrix XenDesktop PowerShell cmdlets have to run under Active Directory domain credentials. Unfortunately for us the DSC Local Configuration Manager (LCM) runs under the LOCALSYSTEM context and Citrix do not provide us with a –Credential parameter [sigh].

This means that we have to resort to using Powershell remoting to connect to local machine with alternate credentials! Unfortunately, this “loopback” mechanism means that if we subsequently attempt to connect to another XenDesktop delivery controller or even the Microsoft SQL server, we are subjected to the “double-hop” restriction.

You can see the “loopback” implementation details in the code. The majority of the custom resources invoke a script block on the local machine when credentials are supplied like so:

if ($Credential) { AddInvokeScriptBlockCredentials -Hashtable $invokeCommandParams -Credential $Credential; }
else { $invokeCommandParams['ScriptBlock'] = [System.Management.Automation.ScriptBlock]::Create($scriptBlock.ToString().Replace('$using:','$')); }
Write-Verbose ($localizedData.InvokingScriptBlockWithParams -f [System.String]::Join("','", @($Name, $Enabled, $Ensure)));
return Invoke-Command @invokeCommandParams;

Here, if the -Credential parameter is supplied in the configuration document then we add splatted parameters to invoke the script block on the local computer via remoting and specifying the credentials. If the –Credential parameter is not supplied then we just execute the script block as-is (after stripping out all the $using: statements).

Example Configuration

You can see an example of the CredSSP implementation in the example files used in the E2E presentation. Here, all node names in the $ConfigurationData that have a role of ‘Controller’ are put into an array (both NetBIOS and FQDN). Finally, the Microsoft SQL server’s name is added to ensure we can delegate credentials to create the databases.

## Need delegated access to all Controllers (NetBIOS and FQDN) and the database server
$credSSPDelegatedComputers = $ConfigurationData.AllNodes | Where Role -eq 'Controller' | ForEach {
    Write-Output $_.NodeName
    if ($_.NodeName.Contains('.')) { ## Output NetBIOS name as well
        Write-Output ('{0}' -f $_.NodeName.Split('.')[0]);
    }
    else { ## Output FQDN as well
        Write-Output ('{0}.{1}' -f $_.NodeName, $ConfigurationData.NonNodeData.XenDesktop.Site.DomainName);
    }
};
$credSSPDelegatedComputers += $ConfigurationData.NonNodeData.XenDesktop.Site.DatabaseServer;

When enumerating the configuration for each delivery controller, for the first delivery controller we create the site and for all other delivery controllers we join the (now existing) site.

node ($AllNodes | Where Role -eq 'Controller' | Select -First 1).NodeName {
    XD7LabSite FirstSiteController {
        Credential = $Credential;
        DatabaseServer = $ConfigurationData.NonNodeData.XenDesktop.Site.DatabaseServer;
        DelegatedComputers = $credSSPDelegatedComputers;
        LicenseServer = ($ConfigurationData.AllNodes | Where Role -eq 'Licensing' | Select -First 1).NodeName;
        SiteAdministrators = $ConfigurationData.NonNodeData.XenDesktop.Site.Administrators;
        SiteName = $ConfigurationData.NonNodeData.XenDesktop.Site.Name;
        XenDesktopMediaPath = $Node.MediaPath;
    }
    ...
}
...
node ($AllNodes | Where Role -eq 'Controller' | Select -Skip 1 | ForEach { $_.NodeName } ) {
    XD7LabController AdditionalSiteController {
        Credential = $Credential;
        DelegatedComputers = $credSSPDelegatedComputers;
        ExistingControllerAddress = ($ConfigurationData.AllNodes | Where Role -eq 'Controller' | Select -First 1).NodeName;
        SiteName = $ConfigurationData.NonNodeData.XenDesktop.Site.Name;
        XenDesktopMediaPath = $Node.MediaPath;
    }
}

The list of computers to permitted to delegate credentials to is then passed to the composite DSC resource with the -DelegatedComputers parameter. Within the CitrixXenDesktop7Lab composite resources you can see the CredSSP implementation where the CredSSP client is actually configured:

Import-DscResource -ModuleName xCredSSP, CitrixXenDesktop7;

xCredSSP CredSSPServer {
    Role = 'Server';
}
    
xCredSSP CredSSPClient {
    Role = 'Client';
    DelegateComputers = $DelegatedComputers;
}

Easy when you know how, eh?! Just remember that how you configure CredSSP is up to you and you don’t have to use the xCredSSP DSC resource; you could use Group Policy if you wanted to. Just remember that if you’re running WMF 4.0 then CredSSP has to be configured somehow!

WMF 5.0

If you’re lucky enough to be able to use WMF 5.0 then all this CredSSP configuration becomes a moot point. In the latest WMF 5.0 preview the Powershell team added a default -PsDscRunAsCredential parameter to all resources that handles the impersonation for us. Yay! \o/

This means that if you use the CitrixXenDesktop7 DSC resources on WMF 5.0 machines you should not specify the –Credential parameter in your configuration, but leverage the built-in -PsDscRunAsCredential parameter instead. This also means that the xCredSSP resources is no longer required and can be removed from your configuration or any composite DSC resources.

Remember, the CitrixXenDeskop7 and CitrixXenDesktop7Lab resources are open-source projects. We would love to see the community contribute, add resources and round out the implementation. Join us and get involved!

Deploying Citrix XenDesktop 7 with DSC

Virtual Engine are pleased to announce that the new DSC resources for Citrix XenDesktop 7 announced at the E2EVC Berlin event can be found here! The example files and presentation have been made available on GitHub. The new Citrix XenDesktop 7 resources include:

  • XD7AccessPolicy
  • XD7Administrator
  • XD7Catalog
  • XD7CatalogMachine
  • XD7Controller
  • XD7Database
  • XD7DesktopGroup
  • XD7DesktopGroupMember
  • XD7EntitlementPolicy
  • XD7Feature
  • XD7Role
  • XD7Site
  • XD7SiteLicense
  • XD7VDAController
  • XD7VDAFeature
  • XD7WaitForSite

The composite resources for lab/development deployments are here and include the following resources:

  • XD7LabController
  • XD7LabDeliveryGroup
  • XD7LabLicenseServer
  • XD7LabMachineCatalog
  • XD7LabSessionHost
  • XD7LabSite
  • XD7LabStorefront
  • XD7LabStorefrontHttps

More information on individual resources can be found in the README.md file of each repository.

This is an open-source community project and so we love to see people join in and contribute! There is still plenty of work to do to add the coverage required – including Storefront. 

Mocking Missing Cmdlet ErrorAction with Pester

Following on from the previous Mocking Missing Cmdlets with Pester post, I also encountered another interesting problem when attempting to mock cmdlets that were not present on the test system. This one is more of an edge-case, hence its own post. Just like last time, the tests worked when I had the Hyper-V cmdlets installed, but failed when running within an Appveyor VM.

It’s probably not uncommon that you will need to ensure that the code under test should throw an error here-and-there. Here is a pseudo-example that tests that Get-VM writes an error when passed with a non-existent VM name:

Describe 'Mocking ErrorAction Example' {
    Function Get-VM { param ($Name) }
    InModuleScope 'xVMHyper-V' {

        It 'Get-VM Throws' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { Write-Error 'Oops' }
            { Get-VM -Name 'TestVM' -ErrorAction Stop } | Should Throw;
        }
    }
}

When this test is run it fails:

Describing Mocking ErrorAction Example
 Write-Error 'Oops'  : Oops
At C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1:709 char:21
+                     & $___ScriptBlock___ @___BoundParameters___ @___ArgumentList ...
+                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException 
 [-] Get-VM Throws 176ms
   Expected: the expression to throw an exception
   at line: 6 in D:\Users\Iain\Desktop\PesterDemo.Tests.ps1
Tests completed in 176ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0

The problem here is that the stub function is not an advanced function and the –ErrorAction preference switch is ignored! This is easily resolved by adding the [CmdletBinding()] attribute to the stub function definition:

Describe 'Mocking ErrorAction Example' {
    Function Get-VM { [CmdletBinding()] param ($Name) }
    InModuleScope 'xVMHyper-V' {

        It 'Get-VM Throws' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { Write-Error 'Oops' }
            { Get-VM -Name 'TestVM' -ErrorAction Stop } | Should Throw;
        }
    }
}

Running the test now results in the expected output:

Describing Mocking ErrorAction Example
 [+] Get-VM Throws 177ms
Tests completed in 177ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

This is not a Pester issue and it’s not a Powershell issue either. It’s just the way the normal Powershell functions work. But, just in case someone else runs into it I thought it would be worth quickly documenting!

Mocking Missing Cmdlets with Pester

When writing Pester unit tests for your Powershell code you will probably have a need to mock calls to external functions before too long. This process works as you would expect when Pester can locate a defined function/cmdlet with a matching name. However, if Pester cannot find a definition, it will fail.

This problem will normally surface in a Continuous Integration (CI) environment. For me, it was writing the first suite of tests for the xHyper-V DSC resource module. The Hyper-V cmdlets where present on my authoring machine but were not present on the Appveyor build VM.

Here is an overly simplified Pester test that I’ll use for demonstration purposes:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        It 'Calls Get-VM' {
            Mock Get-VM -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM -Scope It;
        }

    }
}

If we run the test and Pester cannot locate a defined function then it will report an error. Note: if you run this on a machine with the Hyper-V module installed (and you have a VM called ‘TestVM’) then it will pass – but you knew that already ;).

Describing Missing Cmdlet Mocking Example
 [-] calls Get-VM 474ms
   Could not find Command Get-VM
   at line: 600 in C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1
Tests completed in 474ms

This is easily overcome by defining an empty function within the test file. Note: this will need to be defined within the ‘InModuleScope’ script block if you’re testing a module’s internals.

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { }

        It 'Calls Get-VM' {
            Mock Get-VM -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM -Scope It;
        }

    }
}

The test now passes as we would expect. Yay \o/

Describing Missing Cmdlet Mocking Example
 [+] Calls Get-VM 141ms
Tests completed in 141ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

Now, what you really need to know is that for Pester to enumerate and mock parameter filters, those parameters need to be defined on the stub function. If we were to update the test to check for the passing of a particular –Name parameter like so:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { }

        It 'Calls Get-VM' {
            Mock Get-VM –ParameterFilter { $Name –eq ‘TestVM } -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM –ParameterFilter { $Name –eq ‘TestVM’ } -Scope It;
        }

    }
}

When we run the test it will now fail again.

Describing Missing Cmdlet Mocking Example
 [-] Calls Get-VM 131ms
   Expected Get-VM to be called at least 1 times but was called 0 times
   at line: 518 in C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1
Tests completed in 131ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0

For Pester to enumerate the dynamic parameters on the function, it needs to have the parameters (only the one’s you’re interested in) defined. This can easily be fixed like so:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { param ($Name) }

        It 'Calls Get-VM' {
            Mock Get-VM –ParameterFilter { $Name –eq ‘TestVM } -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM –ParameterFilter { $Name –eq ‘TestVM’ } -Scope It;
        }

    }
}

The tests will now once again pass successfully!

Describing Missing Cmdlet Mocking Example
 [+] Calls Get-VM 169ms
Tests completed in 169ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

Hopefully this helps someone and saves some time. It took me a while to work out what was going on as I had the cmdlets available on my development machine but the tests were failing when running in an Appveyor VM. Perhaps I should submit a pull request to get this put into the Pester help documentation?!

Using the OneGet App-V Provider

Once you have the App-V OneGet/Microsoft Package Management provider installed, how the heck do you use it?!

The App-V provider supports locating, installing and uninstalling App-V 5.x packages from a file path/share (the use of HTTP/S streaming is not supported). To be able install App-V packages we first need to tell the provider where it can find packages.

Note: The following screenshots were taken from a Windows 8.1 x64 WMF 4.0 machine with the OneGet experimental build installed.

Registering a App-V Package Source

Before the App-V provider can locate any packages, you need to register one or more package sources. By default, the only package source that is registered automatically is the App-V Package Source Root, but only if present in the App-V client configuration.

Each package source requires at least a name and location. When registering a package source you will also need to tell OneGet what provider the source is associated with. The following shows adding a local folder as an App-V provider package source.

Register-PackageSource

In addition, each package source can be trusted (they’re not by default). If a package source is untrusted – like the Chocolatey public feed – you will see a prompt when installing packages. When a package source is trusted, you are not prompted when installing packages. The examples shown here do not utilise a trusted package source.

Discovering App-V Packages

The OneGet/Package Management will search all registered package sources by default. For example, searching for ‘Chrome’ will search Chocolatey, the PSGallery and the ‘DropBox’ package source for packages matching ‘Chrome’.

findpackagechrome

That’s quite a few packages and we certainly would not want to install all of them! Fortunately, Find-Package supports filtering packages by provider, version and/or source.

findpackagechromeprovider

findpackagechromesource

findpackagechromeversion

The versioning semantics of App-V packages with OneGet/Package Management will probably be the subject of another blog post. Note: the package summary is read directly from the App-V package description – you do add a description to each sequence right?!

Installing App-V Packages

All packages are installed with the OneGet/PackageManagement Install-Package cmdlet – regardless of provider. There are two primary ways of installing available App-V packages with the App-V provider; Find-Package and Install-Package.

The Install-Package cmdlet will take the output of the Find-Package cmdlet via the pipeline. Therefore, we can install the ‘Microsoft Expression Web’ App-V package using this method.

Note: there are only calls to Get-AppvClientPackage to show that there was nothing registered in the App-V client before Install-Package was run and that the package was registered afterwards. The Get-AppvClientPackage commands are not required!

findpackageinstallpackage

Package names can also be specified on the Install-Package cmdlet itself. This also supports the
–ProviderName, -Source and/or –RequiredVersion switches to filter the installation source etc.

findpackageinstallpackageprovider

Removing App-V Packages

There may be occasions when you might need to remove an App-V package or two. The great thing about the OneGet/Package Management module is that it will enumerate packages registered in the App-V client – regardless of installation method. Therefore, if you already have App-V packages registered, you can still use the App-V OneGet provider without having to make any changes :).

The following example depicts that nothing is registered in the App-V client at first, then Notepad++ is registered outside of OneGet via the native App-V cmdlets and is still detected.

AddAppvClientPackage_GetPackage

Packages can be removed either using the Uninstall-Package cmdlet or piped from the Get-Package cmdlet into the Uninstall-Package cmdlet. The following command will uninstall all registered App-V packages.

UninstallPackage

Unregistering Package Sources

As and when you need to remove package sources, you can use the Unregister-PackageSource cmdlet with the package source name.

UnregisterPackageSource

Note: the OneGet/Package Management App-V provider does not currently support the –Location parameter of the Unregister-PackageSource cmdlet. It is on the backlog and we are working on it 😉

Summary

Remember that this is an open-source project. This means that if there are changes you would like to see implemented or you have found a bug, head over to the project site and start to contribute!

Installing the OneGet App-V Provider

The Microsoft Application Virtualization (App-V) 5.x provider for OneGet can be installed in numerous ways depending on the version of the Windows Management Framework (WMF) installed and/or whether you have the OneGet/Microsoft Package Management bits installed.

Prerequisites

Duh?! The OneGet/Package Management module and the App-V 5.x client of course!

Everything you need is included in the Windows Management Framework 5.0 April 2015 (or later) preview. If you don’t want to install the full WMF 5.0 preview you can download and install the latest experimental OneGet build instead on Windows 7 and upwards.

Note: due to internal namespace changes you will need to have OneGet version 1.15.122.18327 or later installed.

Quick Install

If you have installed WMF 5.0 or the OneGet experimental bits you should be able to install the module via the Powershell Gallery. This process may change once we can get the provider natively bootstrapping. We’re working with @fearthecowboy on this – so watch this space!

To get busy, trying running Install-Module –Name AppvProvider

Manual Install

Until the bootstrapping is working you can manually download and install the provider without having to use PowershellGet.

  1. Download the latest provider release from GitHub.
  2. Unblock the .zip file.
  3. Extract the .zip file contents into the %ProgramFiles%\WindowsPowershell\Modules.

Appv Provider Modules

Is It Working?!

To determine whether you have followed the above instructions correctly and everything is working, fire up a Powershell prompt and run the Get-PackageProvider cmdlet. All being well, you should see the App-V provider listed:

Get-PackageProvider

 

Once you have the provider installed, you can find more details on how to use it in the Using the OneGet App-V Provider post.

Summary

Remember that this is an open-source project. This means that if there are changes you would like to see implemented or you have found a bug, head over to the project page and start to contribute!

App-V Package Management Provider

Appv Provider Project Home Page
Appv Provider Project Home Page

Virtual Engine are proud to announce that we have open-sourced a new Microsoft App-V 5.x OneGet/Package Management provider on GitHub.

This provider complements the built-in providers shipping in the latest Windows Management Framework 5.0 preview, enabling a unified approach to managing package installs on networked computers.  Here is a brief description of OneGet from the online Powershell help.

OneGet is a unified interface for software package management systems; you can run OneGet cmdlets to perform software discovery, installation, and inventory (SDII) tasks. Regardless of the underlying installation technology, you can run the common cmdlets in OneGet to search for, install, or uninstall packages; add, remove, and query package repositories; and run queries on a computer to determine which software packages are installed.

For more information, check out Installing the OneGet App-V Provider and Using the OneGet App-V Provider blog posts.

As this is an open-source project we would love to see the App-V and Powershell communities get involved and contribute. For more information about getting started, check out the project page and join in the fun!

Git and the Powershell ISE

This is one of those seemingly random posts. However, if you use the Git command line tools in the Windows Powershell ISE then this post is for you!

The Windows Git client expects the standard windows console host (conhost.exe) to be used and outputs “stuff” to the stderr stream – even when there is no error. Here’s an example of Git output to the error stream in cmd.exe:

Git in the Windows command prompt

And again in powershell.exe:

Git.exe in the Windows Powershell host

Notice that neither complains of any error. Now – what happens in the Powershell ISE console?

Git.exe in the Windows Powershell ISE host

That’s right folks, git.exe sends that we have successfully switched to the ‘test’ branch to the error stream. Luckily for us the Powershell ISE renders this nicely for us. Not.

PhatGit

This is where the PhatGit module helps out. PhatGit redirects output from the error stream to standard console output. This is rendered by the Powershell ISE console host and can be captured/manipulated etc. If an actual error is encountered then this is still sent to the error stream.

Clean Git.exe output in the Powershell ISE host

In the above screenshot the PhatGit module has been loaded and you can see the first Git command looks like we’d expect and is consistent with the output in the conhost.exe processes from cmd.exe and powershell.exe.

The second command demonstrates that errors are still bubbled up as you would normally expect.

Note: Just in case you’re wondering, “fat Git” is a British English non-complimentary phrase :).

Text Editors

It gets even more complicated – you knew that was coming right?!

Git also relies on external text editors when interactive actions such as merges and commits are performed. Unfortunately, console text editors such as VIM (the default) also rely on the conhost.exe process. If these editors get invoked, then the Powershell ISE console process may become unresponsive – if you don’t believe me try running the following command without PhatGit:

PhatGit without Git commit message

As you can see PhatGit traps known problematic Git commands and stops them hanging the console. The list of known commands easily can be extended but that will have to be another blog post.

Open Source

PhatGit is an open source project and you can find the project over on GitHub. If you would like contribute then get involved! Pull requests are gratefully received and if you find a bug or have any requests, log an issue.

Testing Private Functions with Pester

We’ve been busy beavering away on a new Powershell module that is comprised of many .ps1 files, that are loaded by a master/control .psm1 file when the module is imported. And, like all good Powershell citizens, we have many Pester unit tests for this code in accompanying .Tests.ps1 files.

As Powershell script modules permit us to control which functions should be exported with Export-ModuleMember and Pester allows us to test non-exported functions with the InModuleScope option, you might be wondering why we would ever need to be able to test private/internal functions?

Script Bundles

Whilst coding the new Powershell module, we have always had the desire to ensure that it could also be used as a ‘bundled’ .ps1 file. By bundle, we mean a single combined .ps1 file that can be included verbatim at the beginning of an existing script or by dot-sourcing it as required. Unfortunately – in this particular scenario – the internal module functions would be exposed and could potentially cause unnecessary confusion.

Here’s an example where both the ‘PublicFunction’ and ‘InnerPrivate’ functions would be exposed when bundled or dot-sourced into an existing .ps1 file.

function InnerPrivate {
    param ()
    Write-Output ‘InnerPrivate’
}

function PublicFunction {
    param()
    Write-Output (InnerPrivate)
}

If we only want the ‘PublicFunction’ visible then the simple solution to this is to nest the private function(s) inside the public function(s) like so:

function PublicFunction {
    param()

    function InnerPrivate {
        param ()
        Write-Output ‘InnerPrivate’
    }

   Write-Output (InnerPrivate)
}

Now only the ‘PublicFunction’ will be available. End of the story?

Internal Functions

Not quite; by hiding the functions we now cannot test them with Pester. I had a very brief conversation with Dave Wyatt on GitHub about this and it was agreed that this functionality should not be a part of the official Pester release.

To solve this particular issue, we have a simple function that will locate and return a function’s definition from within a .ps1 file as a script block. The function definition can then be dot-sourced into the current Pester scope to enable testing. Here’s an example Pester test file that will test our ‘InnerPrivate’ function:

$here = Split-Path –Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path –Leaf $MyInvocation.MyCommand.Path).Replace(“.Tests.”, “.”)
# If only testing an internal function, there is no need to dot-source the entire contents..

Describe “InnerPrivate” {
    # Import the ‘InnerPrivate’ function into the current scope
    . (Get-FunctionDefinition –Path “$here\$sut” –Function InnerPrivate)

    It “tests the private, inner function” {
        InnerPrivate | Should Be ‘InnerPrivate’
    }
}

If this code is of interest you can simply save the following code as a .ps1 file in Pester’s \Function directory, for example \Functions\FunctionDefinition.ps1, and Pester will automatically load it.

Notes:

  1. As this code utilises the Abstract Syntax Tree (AST) it does require Powershell 3.0;
  2. If there are any dependencies on variables in the function’s parent scope these will need to be mocked/accounted for;
  3. Depending on how you install/update the Pester module, this might get overwritten when Pester is updated.

When we have more time we’ll put this up on Github so people can collaborate on changes. In the meantime, here’s a copy of the function.

#Requires -Version 3

<#
.SYNOPSIS
    Retrieves a function's definition from a .ps1 file or ScriptBlock.
.DESCRIPTION
    Returns a function's source definition as a Powershell ScriptBlock from an
    external .ps1 file or existing ScriptBlock. This module is primarily
    intended to be used to test private/nested/internal functions with Pester
    by dot-sourcsing the internal function into Pester's scope.
.PARAMETER Function
    The source function's name to return as a [ScriptBlock].
.PARAMETER Path
    Path to a Powershell script file that contains the source function's
    definition.
.PARAMETER LiteralPath
    Literal path to a Powershell script file that contains the source
    function's definition.
.PARAMETER ScriptBlock
    A Powershell [ScriptBlock] that contains the function's definition.
.EXAMPLE
    If the following functions are defined in a file named 'PrivateFunction.ps1'

    function PublicFunction {
        param ()

        function PrivateFunction {
            param ()
            Write-Output 'InnerPrivate'
        }

        Write-Output (PrivateFunction)
    }

    The 'PrivateFunction' function can be tested with Pester by dot-sourcing
    the required function in the either the 'Describe', 'Context' or 'It'
    scopes.

    Describe "PrivateFunction" {
        It "tests private function" {
            ## Import the 'PrivateFunction' definition into the current scope.
            . (Get-FunctionDefinition -Path "$here\$sut" -Function PrivateFunction)
            PrivateFunction | Should BeExactly 'InnerPrivate'
        }
    }
.LINK
    
Testing Private Functions with Pester
#> function Get-FunctionDefinition { [CmdletBinding(DefaultParameterSetName='Path')] [OutputType([System.Management.Automation.ScriptBlock])] param ( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName='Path')] [ValidateNotNullOrEmpty()] [Alias('PSPath','FullName')] [System.String] $Path = (Get-Location -PSProvider FileSystem), [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'LiteralPath')] [ValidateNotNullOrEmpty()] [System.String] $LiteralPath, [Parameter(Position = 0, ValueFromPipeline = $true, ParameterSetName = 'ScriptBlock')] [ValidateNotNullOrEmpty()] [System.Management.Automation.ScriptBlock] $ScriptBlock, [Parameter(Mandatory = $true, Position =1, ValueFromPipelineByPropertyName = $true)] [Alias('Name')] [System.String] $Function ) begin { if ($PSCmdlet.ParameterSetName -eq 'Path') { $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path); } elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') { ## Set $Path reference to the literal path(s) $Path = $LiteralPath; } } # end begin process { $errors = @(); $tokens = @(); if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') { $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock.ToString(), [ref] $tokens, [ref] $errors); } else { $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors); } [System.Boolean] $isFunctionFound = $false; $functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true); foreach ($f in $functions) { if ($f.Name -eq $Function) { Write-Output ([System.Management.Automation.ScriptBlock]::Create($f.Extent.Text)); $isFunctionFound = $true; } } # end foreach function if (-not $isFunctionFound) { if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') { $errorMessage = 'Function "{0}" not defined in script block.' -f $Function; } else { $errorMessage = 'Function "{0}" not defined in "{1}".' -f $Function, $Path; } Write-Error -Message $errorMessage; } } # end process } #end function Get-Function