diff --git a/Scripts/PesterInterface.Tests.ps1 b/Scripts/PesterInterface.Tests.ps1 index 1264a5c4..283b6b2a 100644 --- a/Scripts/PesterInterface.Tests.ps1 +++ b/Scripts/PesterInterface.Tests.ps1 @@ -108,6 +108,21 @@ Describe 'PesterInterface' { $baseMock.Data = @('pester') Expand-TestCaseName $baseMock | Should -Be 'Array TestCase pester' } + It 'Works with Array testcase - placeholder only' { + $baseMock.Name = '<_>' + $baseMock.Data = @('pester') + Expand-TestCaseName $baseMock | Should -Be 'pester' + } + It 'Works with Array testcase - PSCustomObject' { + $baseMock.Name = 'Using PropertyName: Returns ()' + $baseMock.Data = [pscustomobject]@{ Emoji = '🦒' ; Description = 'giraffe' } + Expand-TestCaseName $baseMock | Should -Be 'Using PropertyName: Returns 🦒 (giraffe)' + } + It 'Works with Array testcase - PSCustomObject Property' { + $baseMock.Name = 'Using _.PropertyName: Returns <_.Emoji> (<_.Description>)' + $baseMock.Data = [pscustomobject]@{ Emoji = '🦒' ; Description = 'giraffe' } + Expand-TestCaseName $baseMock | Should -Be 'Using _.PropertyName: Returns 🦒 (giraffe)' + } It 'Works with Single Hashtable testcase' { $baseMock.Name = 'Array TestCase ' $baseMock.Data = @{Name = 'pester' } @@ -118,11 +133,47 @@ Describe 'PesterInterface' { $baseMock.Data = @{Name = 'pester'; Data = 'aCoolTest' } Expand-TestCaseName $baseMock | Should -Be 'Array aCoolTest TestCase pester' } - + It 'Works with Multiple Hashtable testcase - Property syntax' { + $baseMock.Name = 'Using _.PropertyName: Returns <_.Emoji> (<_.Description>)' + $baseMock.Data = @{ Emoji = '🦒' ; Description = 'giraffe' } + Expand-TestCaseName $baseMock | Should -Be 'Using _.PropertyName: Returns 🦒 (giraffe)' + } + It "Works with Multiple Hashtable testcase - dot-navigation" { + $baseMock.Name = 'A () goes ' + $baseMock.Data = @{ Name = "cow"; Animal = @{ Sound = "Mooo"; Emoji = "🐄"}} + Expand-TestCaseName $baseMock | Should -Be 'A 🐄 (cow) goes Mooo' + } It 'Works with Pester.Block' { $Block = Import-Clixml $Mocks/Block.clixml Expand-TestCaseName $Block | Should -Be 'Describe Nested Foreach giraffe' } + It 'Works with Variable defined in Before-Block' -Skip { + $baseMock.Name = ' ' + # TODO dont know how to test this and get this case working + #$baseMock.BeforeAll = { $banana = '🍌' } + #$baseMock.BeforeEach = { $giraffe = '🦒' } + Expand-TestCaseName $baseMock | Should -Be '🍌 🦒' + } + It 'Works with Escaping in Single Quotes' { + $baseMock.Name = 'x: `<<_>`>' + $baseMock.Data = @(1) + Expand-TestCaseName $baseMock | Should -Be 'x: <1>' + } + It 'Works with Escaping in Single Quotes 2' { + $baseMock.Name = 'When x `< 4, x: <_>' + $baseMock.Data = @(1) + Expand-TestCaseName $baseMock | Should -Be 'When x < 4, x: 1' + } + It 'Works with Escaping in Double Quotes' { + $baseMock.Name = "x: ``<<_>``>" + $baseMock.Data = @(1) + Expand-TestCaseName $baseMock | Should -Be 'x: <1>' + } + It 'Works with Escaping in Double Quotes 2' { + $baseMock.Name = "When x ``< 4, x: <_>" + $baseMock.Data = @(1) + Expand-TestCaseName $baseMock | Should -Be 'When x < 4, x: 1' + } } Context 'Get-DurationString' { diff --git a/Scripts/PesterTestPlugin.psm1 b/Scripts/PesterTestPlugin.psm1 index 6fed8361..6d60f313 100644 --- a/Scripts/PesterTestPlugin.psm1 +++ b/Scripts/PesterTestPlugin.psm1 @@ -149,17 +149,62 @@ function Expand-TestCaseName { process { [String]$Name = $Test.Name.ToString() - $Data = Merge-TestData $Test + $Data = ([Hashtable] (Merge-TestData $Test)) + + $flatData = @{} + foreach ($DataItem in $Data) { + $flatData += Flatten-Object $DataItem + } # Array value was stored as _ by Merge-TestData - $Data.GetEnumerator().ForEach{ - $Name = $Name -replace ('<{0}>' -f $PSItem.Key), $PSItem.Value + $placeholders = [regex]::Matches($Name, '(^<|[^`]<)([_\.]?[^>]*)>') + foreach ($placeholder in $placeholders) { + $match = $placeholder.Groups[2].Value + $replacement = "" + if($match.StartsWith("_.")) { + $replacement = $null -ne $flatData[$match] ? $flatData[$match] : $flatData[$match.Trim("_.")] + } else { + $replacement = $null -ne $flatData[$match] ? $flatData[$match] : $flatData["_." + $match] + } + $Name = $Name -replace "<(_\.)?$match>", $replacement } + $Name = $Name -replace '`', '' + return $Name } } +# Inspired by https://gist.github.com/SP3269/fb5b0784bf2cede11ac3a1fa6d5ee1de +function Flatten-Object ( $object, [string] $prefix = "" ) { + [CmdletBinding()] + + $result = @{} + + if ($null -eq $object) { + return @{$prefix = "null" } + } + + $point = $prefix -eq "" ? "" : "." + + switch -Regex ($object.GetType().Name) { + '^(Boolean|String|Int32|Int64|Float|Double|.*\[\])$' { + $result += @{$prefix = $object } + } + "Hashtable" { + $object.GetEnumerator() | ForEach-Object { + $result += Flatten-Object $PSItem.Value ($prefix + $point + $PSItem.Key) + } + } + "PSCustomObject" { + $(Get-Member -InputObject $object -MemberType NoteProperty).GetEnumerator() | ForEach-Object { + $result += Flatten-Object ($object | Select-Object -ExpandProperty $PSItem.Name) ($prefix + $point + $PSItem.Name) + } + } + } + return $result +} + function New-TestItemId { <# .SYNOPSIS diff --git a/sample/Tests/DataDriven.Tests.ps1 b/sample/Tests/DataDriven.Tests.ps1 new file mode 100644 index 00000000..82462035 --- /dev/null +++ b/sample/Tests/DataDriven.Tests.ps1 @@ -0,0 +1,226 @@ +BeforeDiscovery { + $script:arrayOfHashtables = @( + @{ Emoji = '🌵' ; Description = 'cactus' } + @{ Emoji = '🦒' ; Description = 'giraffe' } + @{ Emoji = '🍎' ; Description = 'apple' } + @{ Emoji = '🐧' ; Description = 'penguin' } + @{ Emoji = '😊' ; Description = 'smiling face with smiling eyes' } + ) + + $script:arrayOfObjects = @( + [pscustomobject]@{ Emoji = '🌵' ; Description = 'cactus' } + [System.Object]@{ Emoji = '🦒' ; Description = 'giraffe' } + [pscustomobject]@{ Emoji = '🍎' ; Description = 'apple' } + [pscustomobject]@{ Emoji = '🐧' ; Description = 'penguin' } + [pscustomobject]@{ Emoji = '😊' ; Description = 'smiling face with smiling eyes' } + ) +} + +Describe 'Issue 247' { + Describe 'Template expansion' { + Context 'Array of hashtables' { + It 'Using PropertyName: Returns ()' -ForEach $arrayOfHashtables {} + It 'Using _.PropertyName: Returns <_.Emoji> (<_.Description>)' -ForEach $arrayOfHashtables {} + } + Context 'Array of objects' { + It 'Using PropertyName: Returns ()' -ForEach $arrayOfObjects {} + It 'Using _.PropertyName: Returns <_.Emoji> (<_.Description>)' -ForEach $arrayOfObjects {} + } + } +} + +Describe 'Pester Documentation V5' { + BeforeAll { + Get-Module PesterDemoFunctions | Remove-Module + New-Module PesterDemoFunctions -ScriptBlock { + $emojis = @( + @{ Name = 'apple'; Symbol = '🍎'; Kind = 'Fruit' } + @{ Name = 'beaming face with smiling eyes'; Symbol = '😁'; Kind = 'Face' } + @{ Name = 'cactus'; Symbol = '🌵'; Kind = 'Plant' } + @{ Name = 'giraffe'; Symbol = '🦒'; Kind = 'Animal' } + @{ Name = 'pencil'; Symbol = '✏️'; Kind = 'Item' } + @{ Name = 'penguin'; Symbol = '🐧'; Kind = 'Animal' } + @{ Name = 'pensive'; Symbol = '😔'; Kind = 'Face' } + @{ Name = 'slightly smiling face'; Symbol = '🙂'; Kind = 'Face' } + @{ Name = 'smiling face with smiling eyes'; Symbol = '😊'; Kind = 'Face' } + ) | ForEach-Object { [PSCustomObject]$_ } + + function Get-Emoji ([string]$Name = '*') { + $script:emojis | Where-Object Name -like $Name | ForEach-Object Symbol + } + + function Get-EmojiKind { + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$Emoji + ) + process { + $script:emojis | Where-Object Symbol -eq $Emoji | Foreach-Object Kind + } + } + + $fruitBasket = [System.Collections.ArrayList]@('🍎', '🍌', '🥝', '🥑', '🍇', '🍐') + + function Get-FruitBasket { + $script:fruitBasket + } + + function Remove-FruitBasket { + param( + [Parameter(Mandatory = $true)] + [string]$Item + ) + $script:fruitBasket.Remove($Item) + } + + function Reset-FruitBasket { + $script:fruitBasket = [System.Collections.ArrayList]@('🍎', '🍌', '🥝', '🥑', '🍇', '🍐') + } + } | Import-Module + } + Context "Using -ForEach & -TestCases with hashtable" { + Describe "Get-Emoji" { + It "Returns ()" -ForEach @( + @{ Name = "cactus"; Expected = '🌵' } + @{ Name = "giraffe"; Expected = '🦒' } + @{ Name = "apple"; Expected = '🍎' } + @{ Name = "pencil"; Expected = '✏️' } + @{ Name = "penguin"; Expected = '🐧' } + @{ Name = "smiling face with smiling eyes"; Expected = '😊' } + ) { + Get-Emoji -Name $name | Should -Be $expected + } + } + Describe "Get-Emoji " -ForEach @( + @{ Name = "cactus"; Symbol = '🌵'; Kind = 'Plant' } + @{ Name = "giraffe"; Symbol = '🦒'; Kind = 'Animal' } + ) { + It "Returns " { + Get-Emoji -Name $name | Should -Be $symbol + } + + It "Has kind " { + Get-Emoji -Name $name | Get-EmojiKind | Should -Be $kind + } + } + Describe "Get-Emoji " -ForEach @( + @{ + Name = "cactus"; + Symbol = '🌵'; + Kind = 'Plant' + Runes = @( + @{ Index = 0; Rune = 127797 } + ) + } + @{ + Name = "pencil" + Symbol = '✏️' + Kind = 'Item' + Runes = @( + @{ Index = 0; Rune = 9999 } + @{ Index = 1; Rune = 65039 } + ) + } + ) { + It "Returns " { + Get-Emoji -Name $name | Should -Be $symbol + } + + It "Has kind " { + Get-Emoji -Name $name | Get-EmojiKind | Should -Be $kind + } + + Context "Runes (each character in multibyte emoji)" { + It "Has rune on index " -ForEach $runes { + $actual = @((Get-Emoji -Name $name).EnumerateRunes()) + $actual[$index].Value | Should -Be $rune + } + } + } + } + Context 'Using -ForEach & -TestCases with an array' { + Describe "Get-FruitBasket" { + It "Contains <_>" -ForEach '🍎', '🍌', '🥝', '🥑', '🍇', '🍐' { + Get-FruitBasket | Should -Contain $_ + } + + Context "Fruit <_>" -ForEach '🍎', '🍌', '🥝', '🥑', '🍇', '🍐' { + It "Contains <_> by default" { + Get-FruitBasket | Should -Contain $_ + } + + It "Can remove <_> from the basket" { + Remove-FruitBasket -Item $_ + Get-FruitBasket | Should -Not -Contain $_ + } + } + AfterAll { + Reset-FruitBasket + } + } + } + Context 'Using <> templates' { + BeforeAll { + $script:apple = '🍎' + } + + Describe "" { + It " " -ForEach @( + @{ Animal = "🐛" } + @{ Animal = "🐶" } + ) {} + } + Describe "" { + BeforeAll { + $script:banana = '🍌' + } + + BeforeEach { + $script:giraffe = '🦒' + } + + It " " {} + } + Describe "Animals " { + It "<_>" -ForEach @("🐛", "🐶") {} + } + } + Context 'Using dot-navigation in <> template' { + Describe "Animals" { + It "A () goes " -ForEach @( + @{ + Name = "cow" + Animal = @{ + Sound = "Mooo" + Emoji = "🐄" + } + } + @{ + Name = "fox" + Animal = @{ + Sound = "Ring-ding-ding-ding-dingeringeding!" + Emoji = "🦊" + } + } + ) {} + } + } + Context 'Escaping' { + Describe 'with single quoted string' { + Describe "Fruit <_>" -ForEach "🍎", "🍐" { + It 'Getting <_> returns $null' {} + } + Describe 'When x `< 4, x: <_>' -ForEach @(1..4) { + It 'x: `<<_>`>' {} + } + } + Describe 'with double quoted string' { + Describe "Fruit <_>" -ForEach "🍎", "🍐" { + It "Getting <_> returns `$null" {} + } + Describe "When x ``< 4, x: <_>" -ForEach @(1..4) { + It "x: ``<<_>``>" {} + } + } + } +}