Skip to content

Commit

Permalink
Add Assert assertions to Pester (#2428)
Browse files Browse the repository at this point in the history
Add assertions from Assert and improve them.
  • Loading branch information
nohwnd authored May 21, 2024
1 parent 1a3cad0 commit b577d1b
Show file tree
Hide file tree
Showing 110 changed files with 7,266 additions and 83 deletions.
3 changes: 3 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,12 @@ $script = @(
"$PSScriptRoot/src/Pester.Runtime.ps1"
"$PSScriptRoot/src/TypeClass.ps1"
"$PSScriptRoot/src/Format.ps1"
# TODO: the original version of Format2 from assert, because it does not format strings and other stuff in Pester specific way. I've used this regex (Format-Collection|Format-Object|Format-Null|Format-Boolean|Format-ScriptBlock|Format-Number|Format-Hashtable|Format-Dictionary|Format-Nicely|Get-DisplayProperty|Get-ShortType|Format-Type), '$12' to replace in VS Code.
"$PSScriptRoot/src/Format2.ps1"
"$PSScriptRoot/src/Pester.RSpec.ps1"
"$PSScriptRoot/src/Main.ps1"

"$PSScriptRoot/src/functions/assert/*/*"
"$PSScriptRoot/src/functions/assertions/*"
"$PSScriptRoot/src/functions/*"

Expand Down
106 changes: 106 additions & 0 deletions docs/assertion-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Assert assertions

Pester 6 preview comes with a new set of Should-* assertions. These new assertions are split these categories based on their usage:

- value
- generic
- type specific

- collection
- generic
- combinator

Each of these categories treats `$Actual` and `$Expected` values differently, to provide a consistent behavior when using the `|` syntax.

## Value vs. Collection assertions

The `$Actual` value can be provided by two syntaxes, either by pipeline (`|`) or by parameter (`-Actual`):

```powershell
1 | Should-Be -Expected 1
Should-Be -Actual 1 -Expected 1
```

### Using pipeline syntax

When using the pipeline syntax, PowerShell unwraps the input and we lose the type of the collection on the left side. We are provided with a collection that can be either $null, empty or have items. Notably, we cannot distinguish between a single value being provided, and an array of single item:

```powershell
1 | Should-Be
@(1) | Should-Be
```

These will both be received by the assertion as `@(1)`.

For this reason a value assertion will handle this as `1`, but a collection assertion will handle this input as `@(1)`.

Another special case is `@()`. A value assertion will handle it as `$null`, but a collection assertion will handle it as `@()`.

`$null` remains `$null` in both cases.

```powershell
# Should-Be is a value assertion:
1 | Should-Be -Expected 1
@(1) | Should-Be -Expected 1
$null | Should-Be -Expected $null
@() | Should-Be -Expected $null #< --- TODO: this is not the case right now, we special case this as empty array, but is that correct? it does not play well with the value and collection assertion, and we special case it just because we can.
# $null | will give $local:input -> $null , and @() | will give $local:input -> @(), is that distinction important when we know that we will only check against values?
# This fails, because -Expected does not allow collections.
@() | Should-Be -Expected @()
```powershell
# Should-BeCollection is a collection assertion:
1 | Should-BeCollection -Expected @(1)
@(1) | Should-BeCollection -Expected @(1)
@() | Should-BeCollection -Expected @()
# This fails, because -Expected requires a collection.
$null | Should-BeCollection -Expected $null
```

### Using the -Actual syntax

The value provides to `-Actual`, is always exactly the same as provided.

```powershell
Should-Be -Actual 1 -Expected 1
# This fails, Actual is collection, while expected is int.
Should-Be -Actual @(1) -Expected 1
```

## Value assertions

### Generic value assertions

Generic value assertions, such as `Should-Be`, are for asserting on a single value. They behave quite similar to PowerShell operators, e.g. `Should-Be` maps to `-eq`.

The `$Expected` accepts any input that is not a collection.
The type of `$Expected` determines the type to be used for the comparison.
`$Actual` is automatically converted to that type.

```powershell
1 | Should-Be -Expected $true
Get-Process -Name Idle | Should-Be -Expected "System.Diagnostics.Process (Idle)"
```

The assertions in the above examples will both pass:
- `1` converts to `bool` `$true`, which is the expected value.
- `Get-Process` retrieves the Idle process (on Windows). This process object gets converted to `string`. The string is equal to the expected value.

### Type specific value assertions

Type specific assertions are for asserting on a single value of a given type. For example boolean. These assertions take the advantage of being more specialized, to provide a type specific functionality. Such as `Should-BeString -IgnoreWhitespace`.

The `$Expected` accepts input that has the same type as the assertion type. E.g. `Should-BeString -Expected "my string"`.

`$Actual` accepts input that has the same type as the assertion type. The input is not automatically converted to the destination type, unless the assertion specifies it, e.g. `Should-BeFalsy` will convert to `bool`.

## Collection assertions



These assertions are exported from the module as Assert-* functions and aliased to Should-*, this is because of PowerShell restricting multi word functions to a list of predefined approved verbs.
3 changes: 2 additions & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"sdk": {
"rollForward": "latestFeature",
"version": "8.0.100"
"version": "8.0.100",
"allowPrerelease": false
}
}
194 changes: 194 additions & 0 deletions src/Format2.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
function Format-Collection2 ($Value, [switch]$Pretty) {
$length = 0
$o = foreach ($v in $Value) {
$formatted = Format-Nicely2 -Value $v -Pretty:$Pretty
$length += $formatted.Length + 1 # 1 is for the separator
$formatted
}

$prettyLimit = 50
if ($Pretty -and ($length + 3) -gt $prettyLimit) {
# 3 is for the '@()'
"@(`n $($o -join ",`n ")`n)"
}
else {
"@($($o -join ', '))"
}
}

function Format-Object2 ($Value, $Property, [switch]$Pretty) {
if ($null -eq $Property) {
$Property = foreach ($p in $Value.PSObject.Properties) { $p.Name }
}
$orderedProperty = foreach ($p in $Property | & $SafeCommands['Sort-Object']) {
# force the values to be strings for powershell v2
"$p"
}

$valueType = Get-ShortType $Value
$items = foreach ($p in $orderedProperty) {
$v = ([PSObject]$Value.$p)
$f = Format-Nicely2 -Value $v -Pretty:$Pretty
"$p=$f"
}

if (0 -eq $Property.Length ) {
$o = "$valueType{}"
}
elseif ($Pretty) {
$o = "$valueType{`n $($items -join ";`n ");`n}"
}
else {
$o = "$valueType{$($items -join '; ')}"
}

$o
}

function Format-String2 ($Value) {
if ('' -eq $Value) {
return '<empty>'
}

"'$Value'"
}

function Format-Null2 {
'$null'
}

function Format-Boolean2 ($Value) {
'$' + $Value.ToString().ToLower()
}

function Format-ScriptBlock2 ($Value) {
'{' + $Value + '}'
}

function Format-Number2 ($Value) {
[string]$Value
}

function Format-Hashtable2 ($Value) {
$head = '@{'
$tail = '}'

$entries = foreach ($v in $Value.Keys | & $SafeCommands['Sort-Object']) {
$formattedValue = Format-Nicely2 $Value.$v
"$v=$formattedValue"
}

$head + ( $entries -join '; ') + $tail
}

function Format-Dictionary2 ($Value) {
$head = 'Dictionary{'
$tail = '}'

$entries = foreach ($v in $Value.Keys | & $SafeCommands['Sort-Object'] ) {
$formattedValue = Format-Nicely2 $Value.$v
"$v=$formattedValue"
}

$head + ( $entries -join '; ') + $tail
}

function Format-Nicely2 ($Value, [switch]$Pretty) {
if ($null -eq $Value) {
return Format-Null2 -Value $Value
}

if ($Value -is [bool]) {
return Format-Boolean2 -Value $Value
}

if ($Value -is [string]) {
return Format-String2 -Value $Value
}

if ($value -is [type]) {
return Format-Type2 -Value $Value
}

if (Is-DecimalNumber -Value $Value) {
return Format-Number2 -Value $Value
}

if (Is-ScriptBlock -Value $Value) {
return Format-ScriptBlock2 -Value $Value
}

if (Is-Value -Value $Value) {
return $Value
}

if (Is-Hashtable -Value $Value) {
return Format-Hashtable2 -Value $Value
}

if (Is-Dictionary -Value $Value) {
return Format-Dictionary2 -Value $Value
}

if ((Is-DataTable -Value $Value) -or (Is-DataRow -Value $Value)) {
return Format-DataTable2 -Value $Value -Pretty:$Pretty
}

if (Is-Collection -Value $Value) {
return Format-Collection2 -Value $Value -Pretty:$Pretty
}

Format-Object2 -Value $Value -Property (Get-DisplayProperty2 $Value.GetType()) -Pretty:$Pretty
}

function Get-DisplayProperty2 ([Type]$Type) {
# rename to Get-DisplayProperty?

<# some objects are simply too big to show all of their properties,
so we can create a list of properties to show from an object
maybe the default info from Get-FormatData could be utilized here somehow
so we show only stuff that would normally show in format-table view
leveraging the work PS team already did #>

# this will become more advanced, basically something along the lines of:
# foreach type, try constructing the type, and if it exists then check if the
# incoming type is assignable to the current type, if so then return the properties,
# this way I can specify the map from the most concrete type to the least concrete type
# and for types that do not exist

$propertyMap = @{
'System.Diagnostics.Process' = 'Id', 'Name'
}

$propertyMap[$Type.FullName]
}

function Get-ShortType2 ($Value) {
if ($null -ne $value) {
Format-Type2 $Value.GetType()
}
else {
Format-Type2 $null
}
}

function Format-Type2 ([Type]$Value) {
if ($null -eq $Value) {
return '[null]'
}

$type = [string]$Value

$typeFormatted = $type `
-replace "^System\." `
-replace "^Management\.Automation\.PSCustomObject$", "PSObject" `
-replace "^PSCustomObject$", "PSObject" `
-replace "^Object\[\]$", "collection" `

"[$($typeFormatted)]"
}

function Format-DataTable2 ($Value) {
return "$Value"
}

Loading

0 comments on commit b577d1b

Please sign in to comment.