Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ScriptBlocks as advanced alternative to name templating for data-driven tests #2380

Open
3 tasks done
dkaszews opened this issue Jul 18, 2023 · 4 comments
Open
3 tasks done
Labels

Comments

@dkaszews
Copy link

dkaszews commented Jul 18, 2023

Checklist

Summary of the feature request

I often find myself compromising on the data I use with -ForEach/-TestCases just to get more readable test names. For example:

It '<First> and <Second> return <AreSame> results' -ForEach @(
    @{ First = 'Foo'; Second = 'Bar'; AreSame = 'same' }
    @{ First = 'Foo'; Second = 'Blergh'; AreSame = 'different' } 
) {
    $Expected = $AreSame -eq 'same'
    ...
}

I would prefer to put into data just Expected = $true and Expected = $false, but then I have no way of writing the test template to produce fully gramatical English sentence, as those do not support e.g. ternary operator. Trying to write something like 'return <Expected ? "same" : "different">' just fails to expand the template, or fails the discovery entirely.

One idea I had to (hopefully) easily implement advanced templating without breaking changes is to allow the Name parameter to be a [ScriptBlock] instead. If so, it can be called with all the parameters of the test case. Allowing the user to immediately return a string with full variable substitution should cover most of the cases in a compact way. For example, see below.

How should it work?

It { "${First} and ${Second} return $($Expected ? 'same' : 'different') results" } -ForEach @(
    @{ First = 'Foo'; Second = 'Bar'; Expected = $true } 
    @{ First = 'Foo'; Second = 'Blergh'; Expected = $false } 
) {
    ...
}
@nohwnd
Copy link
Member

nohwnd commented Jul 19, 2023

This could theoretically be done, but

  1. we need a less generic name to be used in that hashtable to represent that scriptblock.
  2. it is not very pleasant to provide that scriptblock for every expansion, and we don't have a more convenient place for it, unless we introduce additional parameter to It, Describe and Context, like --DisplayNameScriptblock.

There is an easy way to get this result, which is simply putting both Expected and AreEqual into the hashtable.


Just for the geeks out there, but I don't recommend using this:

You can also trick the code that does the re-evaluation of the string, by embedding a sub-expression, and have it evaluate any known function or your provided scripblock:

See if you can figure out why it works from the escaping code here:
https://github.com/pester/Pester/blob/main/src/Pester.Runtime.ps1#L392-L405

The only downside there is that you get an extra ` before the expanding, but you can just backspace that 😁

Invoke-pester -container (new-pestercontainer -scriptblock { 
    BeforeDiscovery { 
        function Same ($Same){  $Same -eq "same" ? "`bTHE SAME" : "`bSOOO DIFFERENT"  }
    }
    describe "a" {
        It '<First> and <Second> return `$(Same <AreSame>) results' -ForEach @(
            @{ First = 'Foo'; Second = 'Bar'; AreSame = 'same'; Eval = { param ($a) Write-Host -ForegroundColor Red "-$a-"  } }
            @{ First = 'Foo'; Second = 'Blergh'; AreSame = 'different' } 
        ) {
            # the test
        }

        It '<First> and <Second> return `$(& <Eval> <AreSame>) results' -ForEach @(
            @{ First = 'Foo'; Second = 'Bar'; AreSame = 'same'; Eval = { param ($a) $a -eq "same" ? "`bTHE SAME" : "`bSOOO DIFFERENT" } }
            @{ First = 'Foo'; Second = 'Blergh'; AreSame = 'different'; Eval = { param ($a) $a -eq "same" ? "`bTHE SAME" : "`bSOOO DIFFERENT" } }
        ) {
            # the test
        }
    }
}) -Output Diagnostic
Running tests.
Describing a
  [+] Foo and Bar return THE SAME results 6ms (3ms|4ms)
  [+] Foo and Blergh return SOOO DIFFERENT results 2ms (1ms|1ms)
  [+] Foo and Bar return THE SAME results 6ms (5ms|2ms)
  [+] Foo and Blergh return SOOO DIFFERENT results 5ms (3ms|1ms)
Tests completed in 184ms
Tests Passed: 4, Failed: 0, Skipped: 0 NotRun: 0

@dkaszews
Copy link
Author

dkaszews commented Jul 19, 2023

I think you misunderstood, I am proposing to pass the scriptblock into the -Name parameter, not the -ForEach hashtable for every case. So the syntax is nearly identical as current templating, just wrapped into braces and double quotes instead of singles:

# Old style
It 'Tests with <First> and <Second>' ...
# New style
It {"Tests with $First and $Second"} ...

No new parameters are required, all Pester has to do is check the type of -Name (example code, did not look into internals):

- $DisplayName = ProcessTemplate $Name $TestCase
+ if ($Name is [String]) {
+     $DisplayName = Process-Template $Name $TestCase
+ } else {
+     $Display Name = Invoke-Command $Name -ArgumentList $TestCase
+ }

Maybe some other PowerShell magic will be required to pass the arguments without having to create param(), but you get the idea.

@dkaszews
Copy link
Author

dkaszews commented Jul 19, 2023

An alternative idea with similar syntax is to add a parameter which instead of processing the template will just call $ExecutionContext.InvokeCommand.ExpandString($Name). The examples above become then:

It -ExpandName 'Tests with $First and $Second' ...

I find it less elegant, as it requires extra parameter and use of single quotes to delay expansion, which may look weird and unintuitive.

I don't really understand why Pester did not go into one of those approaches in the first place. It limits the templating to just variables and their children, and the hack you have shown indicates that it is possible to break out of this "sandbox" anyways.

@dkaszews
Copy link
Author

There is an easy way to get this result, which is simply putting both Expected and AreEqual into the hashtable.

By same argument, you could just write the entire test name into an argument and use -Name '<DisplayName>', but that kinda defeats the whole point of templating, doesn't it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants