├── .vscode └── tasks.json ├── Examples.ps1 ├── LICENSE ├── PesterMatchHashtable.Tests.ps1 ├── PesterMatchHashtable.ps1 ├── PesterMatchHashtable.psd1 ├── PesterMatchHashtable.psm1 ├── Publish.ps1 ├── README.md ├── RunPester.ps1 └── appveyor.yml /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | { 9 | "version": "0.1.0", 10 | "command": "powershell", 11 | "isShellCommand": true, 12 | "showOutput": "silent", 13 | "suppressTaskName": true, 14 | "tasks": [ 15 | { 16 | "taskName": "Test", 17 | "isTestCommand": true, 18 | "args": [ 19 | // weird as it seems, we're passing these as separate params as otherwise 20 | // VSCode will put them in quotes and then PowerShell no longer recognises 21 | // them as switches! 22 | "-ExecutionPolicy", "RemoteSigned", 23 | "-Command", "${workspaceRoot}\\RunPester.ps1" 24 | ], 25 | "problemMatcher": { 26 | "owner": "pester", 27 | "pattern": { 28 | "regexp": "(.+);(.+);(.*)", 29 | "file": 1, 30 | "line": 2, 31 | "message": 3 32 | } 33 | } 34 | }, 35 | { 36 | "taskName": "Package", 37 | "isBuildCommand": true, 38 | "args": [ 39 | "-ExecutionPolicy", "RemoteSigned", 40 | "-Command", "${workspaceRoot}\\BuildPackage.ps1" 41 | ] 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /Examples.ps1: -------------------------------------------------------------------------------- 1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 2 | Import-Module "$here\PesterMatchHashtable.psm1" -Force 3 | 4 | Describe "MatchHashtable examples" { 5 | It "single item hashtables match" { 6 | @{"a"=1} | Should MatchHashtable @{"a"=1} 7 | } 8 | It "hashtables with the same contents match" { 9 | @{"a"=1;"b"="wibble"} | Should MatchHashtable @{"b"="wibble";"a"=1} 10 | } 11 | It "hashtables with different lengths do not match" { 12 | @{"a"=1} | Should Not MatchHashtable @{"b"="wibble";"a"=1} 13 | } 14 | It "hashtables with different lengths do not match" { 15 | @{"a"=1;"b"="wibble"} | Should Not MatchHashtable @{"b"="wibble"} 16 | } 17 | It "hashtables with different values do not match" { 18 | @{"a"=1;"b"="wibble"} | Should Not MatchHashtable @{"a"=123; "b"="wibble"} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stuart Leeks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /PesterMatchHashtable.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 | 6 | ## Test Assertion functions takenb from: https://github.com/pester/Pester/blob/ebfb0997365fea29f25b2aa3065378a3765eff4c/Functions/Assertions/Test-Assertion.ps1 7 | function Test-PositiveAssertion($result) { 8 | if (-not $result) { 9 | throw "Expecting expression to pass, but it failed" 10 | } 11 | } 12 | 13 | function Test-NegativeAssertion($result) { 14 | if ($result) { 15 | throw "Expecting expression to pass, but it failed" 16 | } 17 | } 18 | 19 | Describe "PesterMatchHashtable" { 20 | It "returns true for matching single item hashtable with same values" { 21 | @{"a" = 1} | Should MatchHashtable @{"a" = 1} 22 | } 23 | It "returns true for hashtable with the same contents" { 24 | @{"a" = 1; "b" = "test"} | Should MatchHashtable @{"a" = 1; "b" = "test"} 25 | } 26 | It "returns true for hashtable with the same contents in different orders" { 27 | @{"a" = 1; "b" = "test"} | Should MatchHashtable @{"b" = "test"; "a" = 1} 28 | } 29 | 30 | It "returns false if hashtable differ in content" { 31 | @{"a" = 1; "b" = "test"} | Should Not MatchHashtable @{"a" = 1; "b" = "different value"} 32 | } 33 | It "returns false if hashtable differ in length - input2 longer" { 34 | @{"a" = 1} | Should Not MatchHashtable @{"a" = 1; "b" = "test"} 35 | } 36 | It "returns false if hashtable differ in length - input1 longer" { 37 | @{"a" = 1; "b" = "test"} | Should Not MatchHashtable @{"a" = 1} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /PesterMatchHashtable.ps1: -------------------------------------------------------------------------------- 1 | function PesterMatchHashtable($ActualValue, $ExpectedValue, [switch] $Negate) { 2 | $message = FindMismatchedHashtableValue $ActualValue $ExpectedValue 3 | $success = $message -eq $null; 4 | 5 | if ($success) { 6 | if ($Negate) { 7 | #expecting failure 8 | $success = $false 9 | $message = "Expected: ${ActualValue} to not match the expression ${ExpectedValue}" 10 | } 11 | # else - we can just return success 12 | } 13 | else { 14 | if ($Negate) { 15 | # expecting failure 16 | $success = $true 17 | $message = "" 18 | } 19 | # else - we can just return failure 20 | } 21 | return New-Object psobject -Property @{ 22 | Succeeded = $success 23 | FailureMessage = $message 24 | } 25 | } 26 | 27 | function FindMismatchedHashtableValue($ActualValue, $ExpectedValue) { 28 | foreach($expectedKey in $ExpectedValue.Keys) { 29 | if (-not($ActualValue.Keys -contains $expectedKey)){ 30 | return "Expected key: {$expectedKey}, but missing in actual" 31 | } 32 | $expectedItem = $ExpectedValue[$expectedKey] 33 | $actualItem = $ActualValue[$expectedKey] 34 | if (-not ($actualItem -eq $expectedItem)) { 35 | return "Value differs for key {$expectedKey}. Expected value: {$expectedItem}, actual value: {$actualItem}" 36 | } 37 | } 38 | 39 | foreach($actualKey in $ActualValue.Keys) { 40 | if (-not($ExpectedValue.Keys -contains $actualKey)){ 41 | return "Actual key: {$actualKey}, but missing in expected" 42 | } 43 | $expectedItem = $ExpectedValue[$actualKey] 44 | $actualItem = $ActualValue[$actualKey] 45 | if (-not ($actualItem -eq $expectedItem)) { 46 | return "Value differs for key {$actualKey}. Expected value: {$expectedItem}, actual value: {$actualItem}" 47 | } 48 | } 49 | } 50 | 51 | function NotPesterMatchHashtableFailureMessage($ActualValue, $ExpectedValue) { 52 | return "Expected: ${ActualValue} to not match the expression ${ExpectedValue}" 53 | } 54 | 55 | Add-AssertionOperator -Name MatchHashtable ` 56 | -Test $function:PesterMatchHashtable -------------------------------------------------------------------------------- /PesterMatchHashtable.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'PesterMatchHashtable' 3 | # 4 | # Generated by: Stuart Leeks 5 | # 6 | # Generated on: 11/07/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'PesterMatchHashtable' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.3.0' 16 | 17 | # ID used to uniquely identify this module 18 | GUID = 'bf155940-82fe-4eb2-a053-40292fcbf3f8' 19 | 20 | # Author of this module 21 | Author = 'Stuart Leeks' 22 | 23 | # Company or vendor of this module 24 | CompanyName = 'Unknown' 25 | 26 | # Copyright statement for this module 27 | Copyright = '(c) 2016 Stuart Leeks. All rights reserved.' 28 | 29 | # Description of the functionality provided by this module 30 | Description = 'Hashtable assertions for Pester' 31 | 32 | # Minimum version of the Windows PowerShell engine required by this module 33 | # PowerShellVersion = '' 34 | 35 | # Name of the Windows PowerShell host required by this module 36 | # PowerShellHostName = '' 37 | 38 | # Minimum version of the Windows PowerShell host required by this module 39 | # PowerShellHostVersion = '' 40 | 41 | # Minimum version of Microsoft .NET Framework required by this module 42 | # DotNetFrameworkVersion = '' 43 | 44 | # Minimum version of the common language runtime (CLR) required by this module 45 | # CLRVersion = '' 46 | 47 | # Processor architecture (None, X86, Amd64) required by this module 48 | # ProcessorArchitecture = '' 49 | 50 | # Modules that must be imported into the global environment prior to importing this module 51 | RequiredModules = @('Pester') 52 | 53 | # Assemblies that must be loaded prior to importing this module 54 | # RequiredAssemblies = @() 55 | 56 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 57 | # ScriptsToProcess = @() 58 | 59 | # Type files (.ps1xml) to be loaded when importing this module 60 | # TypesToProcess = @() 61 | 62 | # Format files (.ps1xml) to be loaded when importing this module 63 | # FormatsToProcess = @() 64 | 65 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 66 | # NestedModules = @() 67 | 68 | # Functions to export from this module 69 | FunctionsToExport = '*' 70 | 71 | # Cmdlets to export from this module 72 | CmdletsToExport = '*' 73 | 74 | # Variables to export from this module 75 | VariablesToExport = '*' 76 | 77 | # Aliases to export from this module 78 | AliasesToExport = '*' 79 | 80 | # List of all modules packaged with this module 81 | # ModuleList = 'PesterMatchHashtable' 82 | 83 | # List of all files packaged with this module 84 | FileList = @('PesterMatchHashtable.ps1', 'PesterMatchHashtable.psm1') 85 | 86 | # Private data to pass to the module specified in RootModule/ModuleToProcess 87 | PrivateData = @{ 88 | 89 | PSData = @{ 90 | 91 | # Tags applied to this module. These help with module discovery in online galleries. 92 | Tags = @('Pester', 'MatchHashtable') 93 | 94 | # A URL to the license for this module. 95 | LicenseUri = 'https://github.com/stuartleeks/PesterMatchHashtable/blob/master/LICENSE' 96 | 97 | # A URL to the main website for this project. 98 | ProjectUri = 'https://github.com/stuartleeks/PesterMatchHashtable' 99 | 100 | # A URL to an icon representing this module. 101 | # IconUri = '' 102 | 103 | # ReleaseNotes of this module 104 | # ReleaseNotes = '' 105 | 106 | } # End of PSData hashtable 107 | 108 | } # End of PrivateData hashtable 109 | 110 | # HelpInfo URI of this module 111 | # HelpInfoURI = '' 112 | 113 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 114 | # DefaultCommandPrefix = '' 115 | 116 | } 117 | 118 | -------------------------------------------------------------------------------- /PesterMatchHashtable.psm1: -------------------------------------------------------------------------------- 1 | Push-Location $PSScriptRoot 2 | . .\PesterMatchHashtable.ps1 3 | Pop-Location -------------------------------------------------------------------------------- /Publish.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [Parameter(Mandatory=$true)] 3 | [string]$powershellGalleryKey 4 | ) 5 | Publish-Module -NuGetApiKey $powershellGalleryKey -Path ./ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PesterMatchHashtable 2 | This module provides additional assertions for [Pester](https://github.com/pester/Pester) to help with testing hash table contents 3 | 4 | Also see [PesterMatchArray](https://github.com/stuartleeks/PesterMatchArray) for array assertions. 5 | 6 | [![Build status](https://ci.appveyor.com/api/projects/status/50vdbyyjaorv8c6v/branch/master?svg=true)](https://ci.appveyor.com/project/stuartleeks/pestermatchhashtable/branch/master) 7 | 8 | ## Installation 9 | 10 | ### PowerShell Gallery 11 | 12 | You can install [PesterMatchHashtable](https://www.powershellgallery.com/packages/PesterMatchHashtable/) via the [PowerShell Gallery](https://www.powershellgallery.com/) 13 | 14 | ```powershell 15 | Install-Module -Name PesterMatchHashtable 16 | ``` 17 | 18 | ## Examples 19 | 20 | MatchHashtable compares two hashtables. 21 | 22 | ```powershell 23 | Describe "MatchHashtable examples" { 24 | It "single item hashtables match" { 25 | @{"a"=1} | Should MatchHashtable @{"a"=1} 26 | } 27 | It "hashtables with the same contents match" { 28 | @{"a"=1;"b"="wibble"} | Should MatchHashtable @{"b"="wibble";"a"=1} 29 | } 30 | It "hashtables with different lengths do not match" { 31 | @{"a"=1} | Should Not MatchHashtable @{"b"="wibble";"a"=1} 32 | } 33 | It "hashtables with different lengths do not match" { 34 | @{"a"=1;"b"="wibble"} | Should Not MatchHashtable @{"b"="wibble"} 35 | } 36 | It "hashtables with different values do not match" { 37 | @{"a"=1;"b"="wibble"} | Should Not MatchHashtable @{"a"=123; "b"="wibble"} 38 | } 39 | } 40 | ``` 41 | 42 | ## Release Notes 43 | 44 | ### Version 0.3.0 27th July 2017 45 | Pushed to PowerShell Gallery 46 | 47 | Updated to work with Pester v4 (tested on 4.0.5 pre-release) 48 | 49 | ### Version 0.0.1 11th July 2016 50 | Pushed to PowerShell Gallery 51 | 52 | Initial version of PesterMatchHashtable 53 | 54 | ## Future 55 | * Wrap up to publish to chocolatey, psget -------------------------------------------------------------------------------- /RunPester.ps1: -------------------------------------------------------------------------------- 1 | function GetLineNumber($stackTrace){ 2 | if($stackTrace -match "at line: (\d*)"){ 3 | $matches[1]; 4 | } else { 5 | $null 6 | } 7 | } 8 | function GetFileName($stackTrace){ 9 | if($stackTrace -match "at line: (?:\d*) in (.*)$"){ 10 | $matches[1]; 11 | } else { 12 | $null 13 | } 14 | } 15 | function FormatResult ($result){ 16 | process { 17 | $lineNumber = GetLineNumber $_.StackTrace 18 | $file = GetFileName $_.StackTrace | Resolve-Path -Relative 19 | $collapsedMessage = $_.FailureMessage -replace "`n"," " 20 | $testDescription = "$($_.Describe):$($_.Name)" 21 | "$file;$lineNumber;${testDescription}:$collapsedMessage" 22 | } 23 | } 24 | Write-Output "Running tests..." 25 | $results = Invoke-Pester -PassThru # can use -Quiet to suppress the default Pester output 26 | $results.TestResult | ?{ -not $_.Passed} | FormatResult 27 | Write-Output "Done" -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | image: WMF 5 3 | install: 4 | - ps: >- 5 | Get-PackageProvider -Name NuGet -Force 6 | 7 | Install-Module -Name Pester -Force 8 | 9 | build_script: 10 | - ps: Invoke-Pester -EnableExit --------------------------------------------------------------------------------