Skip to content

RFC: Better API for Invoke-Pester #1265

Description

@nohwnd

I am trying to simplify the interfaces on Invoke-Pester, to create a pit of success, where the usual stuff is simple easy to discover, and advanced stuff is less simple and less easy to discover. The way Pester v4 does that (to some extent) is by hiding hashtables behind parameters, this gives you a lot of freedom, but hardly a clean strongly typed interface. What I would like is a simple interface to be easy to start with, and the configurations to be explicit so I don't have to remember structure of hashtables. It would still be nice to have hashtables, so they can be used as literals in the code, modified and passed around easily.

Simple

What I am assuming is that a typical person has a folder with tests, and want's to do some basic filtering on them. They also want to run them locally to develop, and run them on a build server easily. To me the simplest non restrictive parameter set is:

Invoke-Pester 
 -Path <string[]>
 -ExcludePath <string[]>
 -Name <string[]>
 -ExcludeName <string[]>
 -Tag <string[]>
 -ExcludeTag <string[]>
 -PassThru <switch>
 -Parallel <switch>
 -Watch <switch>
 -Ci <switch>
 -Output <Normal, Short, None>

This would cover 99% of what I do. 😁

This minimal api would then be typically used like this:

Invoke-Pester
Invoke-Pester -Watch
Invoke-Pester -Path C:\projects\mymodule
Invoke-Pester -Path . -Ci
Invoke-Pester -Path . -Ci -PassThru | Export-NunitXML -Path "TestResults.xml"
Invoke-Pester -Tag "Integration*" -Ci -PassThru | 
    Export-NunitXML -Path "TestResults.xml"

Invoke-Pester -Path "*.Integration.Tests.ps1" -Ci -PassThru | 
    Export-NunitXML -Path "TestResults.xml"

Invoke-Pester
List of paths to run *.Tests.ps1 from. When the path is a folder it will run all *.Tests.ps1 files in it and its subfolders. When the path is a wildcarded file like c:\tests\*.ps1 it will run all *.Tests.ps1 in that folder, if that path is a full path, it will run the path, even if it is not a .Tests.ps1 file.
Defaults to the current directory "." (Just like in v4).
-Path <string[]>

Excludes paths from the test run, it uses -like wildcards to match the paths, and automatically replaces '' to '/' to make it work cross-plaform easily.
-ExcludePath <string[]>

Filters on Block and Test name, uses . as a delimiter for the path. I think this is a useful and easy to use convention. For example having a test 'it1' in 'describe2'that is inside of 'describe1' would give the test a full path of describe1.describe2.it1, and we can then filter like this: 'it1' to run just it, or describe1.* to run everything in that block, or *.describe2.* to run everything in describe2 block no matter where it is placed.
-Name <string[]>
Same as above, but it excludes the tests that match. Exclude takes precedence before any other filter.
-ExcludeName <string[]>

Filters blocks and tests based on their tags. If any tag matches the test will run. Accepts like wildcards.
-Tag <string[]>
Same as above but it excludes tags.
-ExcludeTag <string[]>

Outputs the test result
-PassThru <switch>

Runs the tests in parallel (with per-file granularity), the number allows you to specify how many tests should run in parallel, defaults to your core count.
-Parallel <int>

Watch the given file paths, and if any file changes wait 2s and then run the tests in it
-Watch <switch>

A single switch that combines multiple things that are used in CI builds. Run the tests in strict mode (only passed/failed results are permitted but not Skip etc.). Do NOT focus tests/blocks. Exit with exit code if any test fails.
-Ci <switch>

This controls the verbosity of the on-screen output. The naming clashes with what is already there, and the goal is not to give all the options as in v4 here, because there might me more options, the output should be customizable, and because different modes (e.g. normal run, vs a parallized watch run) might need different output formatting to make sense. So say we go with the first:
-Output Normal, Short, None (or -Show?)

Then Normal would show, the complete output.
Short would show the failed tests that passed per block and then it would print failed tests.

Describing abc
    [-] err 1
    [-] err 2
    [-] err 3
    [+] and 100 passed tests

There is still option to add more verbosity before (like Trace, Debugging, Detailed) and in the middle, like Minimal, but let's not do that now, especially with the possibility that the output will be customizable.

Other possible namings
-Output Detailed, Normal, Minimal, Quiet
-Output Trace, Debugging, Normal, Short, Minimal, None

Advanced

For advanced stuff there would be functions that would generate the objects you need to provide, that way we avoid having confusing api that uses Object for parameters just so you can provide a string / string[] / scriptblock / hashtable / hashtable with aliases to a -Script parameter that is used as -Path <string> 90% of the time anyway.

So for example defining an advanced filter would be:

# New-FilterOption?
# New-PesterFilter?
New-Filter 
 -Tag <string[]>
 -ExcludeTag <string[]>
 -MustHaveAllTags -Name <string[]>
 -ExcludeName <string[]>
 -ScriptBlock <scriptblock>
 -LiteralPath <string[]>
 -ExcludeLiteralPath<string[]>
 -Position <string[]>
 -ExcludePosition<string[]>

New-Filter -AsHashtable <switch>
New-Filter -AsHashtableLiteral <switch>

Invoke-Pester -Filter <Object>

The New-Filter would produce an object that would contain all the filter parameters you specified. This has few advantages over pure hashtables:

  1. you don't have to remember what hashtable pester takes, because you get them from intellisense.
  2. it avoids parameter boom on Invoke-Pester because it does not have to be extended every time.

There might also be switches that would return the object as a hashtable / template so you can manipulate it easily by code, or write it as a literal and then splat it.

e.g. New-Filter -AsHashtableLiteral | clip

# paste it here and set what you need,
# e.g. I want all integraion tests for PowerShell Core
$filter = @{
    Tag = "Integration", "Core"
    MustHaveAllTags = $true
}

The same approach would be applied to other advanced stuff like:

  • providing test file paths, and parameters to use when running them
  • code coverage
  • debugging options
  • run options (e.g. disabling TestDrive, and enabling it only when you need it)
  • plugins?

It would also be a nice step towards configuration because

In between?

There is probably a huge gap between the simple interface, and the advanced interface, is there another one needed? How would it look like? How it would be implemented to not overwhelm the beginner trying to understand what to do?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions