Test-Member - The Missing PowerShell Cmdlet

by Klaus Graefensteiner 29. April 2010 13:16

Introduction

I was working on a SkyTap REST API  client implementation in PowerShell and spent quite some time understanding the HTTPWebRequest object of the .Net Framework. The HTTPWebRequest object has a generic way to modify the HTTP Request headers and also some convenient properties to directly access the most common header values like for example the request method (GET, POST, PUT, DELETE etc.).

In my PowerShell scripts I wanted to use an convenient property, if it is available and if it isn’t modify the Headers collection instead. In order to do this I needed to check whether a property exists or not. This blog post is about my version of the Test-Member cmdlet.

DSC01398

Figure 1: The missing cmdlet

Features

This short cmdlet demonstrates some valuable advanced functions features:

  • Advanced parameter binding
  • Parameter sets
  • Parameter validation
  • PSUnit unit tests

Test-Member

Here is the script:


function Test-Member()
{
<# 
.Synopsis 
    Verifies whether a specific property or method member exists for a given .NET object 
.Description 
    Verifies whether a specific property or method member exists for a given .NET object  
.Example
    Test-Member -PropertyName "Context" -InputObject (new-object System.Collections.ArrayList)
.Example
    new-object "System.Collections.ArrayList" | Test-Member -PropertyName "ToArray"
.Parameter PropertyName
    Name of a Property to test for 
.Parameter MethodName
    Name of a Method to test for  
.ReturnValue 
    $True or $False 
.Link 
    about_functions_advanced 
    about_functions_advanced_methods 
    about_functions_advanced_parameters 
.Notes 
NAME:      Test-Member 
AUTHOR:    Klaus Graefensteiner 
LASTEDIT:  04/20/2010 12:12:42
#Requires -Version 2.0 
#> 
    
    [CmdletBinding(DefaultParameterSetName="Properties")]
    PARAM(
        
        [ValidateNotNull()]
        [Parameter(Position=0, Mandatory=$True, ValueFromPipeline=$True)]
        $InputObject,
        
        [ValidateNotNullOrEmpty()]
        [Parameter(Position=1, Mandatory=$True, ValueFromPipeline=$False, ParameterSetName="Properties")]
        [string] $PropertyName,
        
        [ValidateNotNullOrEmpty()]
        [Parameter(Position=1, Mandatory=$True, ValueFromPipeline=$False, ParameterSetName="Methods")]
        [string] $MethodName      
    )
    Process{
        
        switch ($PsCmdlet.ParameterSetName) 
        { 
            "Properties"
            {
                $Members = Get-Member -InputObject $InputObject;
                if ($Members -ne $null -and $Members.count -gt 0)
                {
                    foreach($Member in $Members)
                    {
                        if(($Member.MemberType -eq "Property" ) -and ($Member.Name -eq $PropertyName))
                        {
                            return $true
                        }
                    }
                    return $false
                }
                else
                {
                    return $false;
                }
            }
            "Methods"
            {
                $Members = Get-Member -InputObject $InputObject;
                if ($Members -ne $null -and $Members.count -gt 0)
                {
                    foreach($Member in $Members)
                    {
                        if(($Member.MemberType -eq "Method" ) -and ($Member.Name -eq $MethodName))
                        {
                            return $true
                        }
                    }
                    return $false
                }
                else
                {
                    return $false;
                }
            }
        }
    
    }# End Process
}


function Parse-XML([string] $XMLString, [String] $XPath, [int] $Status )
{
    $XMLResult = [XML] $XMLString;
    
    if($XMLResult -ne $null)
    {
        if($Status -eq 200)
        {
            $XPathSingle = $XPath.substring(0, $XPath.Length -1)
            return $XMLResult.$XPath.$XPathSingle;
        }
        else
        {
            return $XMLResult.Error;
        }
    }
}

Unit Tests

Here are the unit tests:

. PSUnit.ps1

function Test.InputObjectFromPipelineReturnsTrueWhenLookingForToArrayMethodInArrayList()
{
    #Arrange
    #Act
    $Result = new-object "System.Collections.ArrayList" | Test-Member -MethodName "ToArray"
    
    #Assert
    Assert-That -ActualValue $Result -Constraint {$ActualValue -eq $True}
}

function Test.InputObjectFromPipelineReturnsFalseWhenLookingForToArrayPropertyInArrayList()
{
    #Arrange
    #Act
    $Result = new-object "System.Collections.ArrayList" | Test-Member -PropertyName "ToArray"
    
    #Assert
    Assert-That -ActualValue $Result -Constraint {$ActualValue -eq $False}
}

function Test.InputObjectFromPipelineReturnsTrueWhenLookingForCountPropertyInArrayList()
{
    #Arrange
    #Act
    $Result = new-object "System.Collections.ArrayList" | Test-Member -PropertyName "Count"
    
    #Assert
    Assert-That -ActualValue $Result -Constraint {$ActualValue -eq $True}
}

function Test.InputObjectFromPipelineReturnsFalseWhenLookingForCountMethodInArrayList()
{
    #Arrange
    #Act
    $Result = new-object "System.Collections.ArrayList" | Test-Member -MethodName "Count"
    
    #Assert
    Assert-That -ActualValue $Result -Constraint {$ActualValue -eq $False}
}

Download

The Cmdlet can be downloaded here: Test-Member.zip

Ausblick

That’s it.

Tags: , ,

PowerShell | Tips & Tricks

Comments

4/29/2010 2:40:31 PM #

Jeffrey Snover


Very nice concept. Very nice code.
One thought - this will tell you whether the .NET class as that property but not whether PowerShell treats it as though it has that property.

For instance, System.Diagnostics.Process has a property PROCESSNAME but PowerShell also treats it as though it as a property called NAME.  This is an ALIASPROPERTY.  (you can see this by doing:  GPS | GM *NAME )

If you want test these, you want to change your test from -EQ "Property" to
-like "*Property"  (we have PROPERTY, CODEPROPERTY, NOTEPROPERTY, SCRIPTPROPERTY and  PARAMETERIZEDPOPERTY)

Great stuff.

Experiment!  Enjoy!  Engage!

Jeffrey Snover [MSFT]
Distinguished Engineer
Visit the Windows PowerShell Team blog at:    http://blogs.msdn.com/PowerShell
Visit the Windows PowerShell ScriptCenter at:  www.microsoft.com/.../msh.mspx

Jeffrey Snover United States |

5/5/2010 2:35:57 AM #

Klaus Graefensteiner

Thanks for this great feedback.

Klaus

Klaus Graefensteiner |

5/4/2010 11:25:37 PM #

Poshoholic

I like the concept as well.  One thing I would suggest though would be to use a proxy function for Get-Member instead of rolling your own function.  Proxy functions are really useful, and by using a proxy function you get the benefit of all of the parameters for Get-Member while still only returning true or false if a member exists.  You can validate the existence of members of any type this way, of specific types, or of multiple types (using wildcards like *property as Jeffrey Snover suggested).  You can also use wildcards in the member name as well.

Here's my Test-Member proxy function that I wrote using PowerGUI's proxy function snippet:

function Test-Member {
  <#
  .ForwardHelpTargetName Get-Member
  .ForwardHelpCategory Cmdlet
  #>
  [CmdletBinding()]
  param(
    [Parameter(ValueFromPipeline=$true)]
    [System.Management.Automation.PSObject]
    ${InputObject},

    [Parameter(Position=0)]
    [ValidateNotNullOrEmpty()]
    [System.String[]]
    ${Name},

    [Alias('Type')]
    [System.Management.Automation.PSMemberTypes]
    ${MemberType},

    [System.Management.Automation.PSMemberViewTypes]
    ${View},

    [Switch]
    ${Static},

    [Switch]
    ${Force}
  )
  begin {
    try {
      $outBuffer = $null
      if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
      {
        $PSBoundParameters['OutBuffer'] = 1
      }
      $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-Member', [System.Management.Automation.CommandTypes]::Cmdlet)
      $scriptCmd = {& $wrappedCmd @PSBoundParameters | ForEach-Object -Begin {$members = @()} -Process {$members += $_} -End {$members.Count -ne 0}}
      $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
      $steppablePipeline.Begin($PSCmdlet)
    }
    catch {
      throw
    }
  }
  process {
    try {
      $steppablePipeline.Process($_)
    }
    catch {
      throw
    }
  }
  end {
    try {
      $steppablePipeline.End()
    }
    catch {
      throw
    }
  }
}

Poshoholic Canada |

5/5/2010 2:35:05 AM #

Klaus Graefensteiner

Thanks,

This is just too cool. All add this technique to my PowerShell toolbox.

Regards,

Klaus

Klaus Graefensteiner United States |

5/15/2010 3:25:05 AM #

pingback

Pingback from dougfinke.com

PowerShell Hacker #1

dougfinke.com |

5/15/2010 3:59:49 AM #

pingback

Pingback from blog.lab49.com

PowerShell Hacker #1 » Lab49 Blog

blog.lab49.com |

Comments are closed

About Klaus Graefensteiner

I like the programming of machines.

Add to Google Reader or Homepage

LinkedIn FacebookTwitter View Klaus Graefensteiner's profile on Technorati
Klaus Graefensteiner

Klaus Graefensteiner
works as Developer In Test and is founder of the PowerShell Unit Testing Framework PSUnit. More...

Open Source Projects

PSUnit is a Unit Testing framwork for PowerShell. It is designed for simplicity and hosted by Codeplex.
BlogShell is The tool for lazy developers who like to automate the composition of blog content during the writing of a blog post. It is hosted by CodePlex.

Administration

About

Powered by:
BlogEngine.Net
Version: 1.6.1.0

License:
Creative Commons License

Copyright:
© Copyright 2012, Klaus Graefensteiner.

Disclaimer:
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

Theme design:
This blog theme was designed and is copyrighted 2012 by Klaus Graefensteiner

Rendertime:
Page rendered at 2/5/2012 10:59:29 PM (PST Pacific Standard Time UTC DST -7)