haf / expecto

A smooth testing lib for F#. APIs made for humans! Strong testing methodologies for everyone!
Apache License 2.0
663 stars 96 forks source link

An observation / question about using testTheory with 'null' and '"" as inputs #493

Closed Numpsy closed 2 months ago

Numpsy commented 3 months ago

An observation whilst trying to do a few simple error case input tests with testTheory -

I noticed that if I have test code like

[<Tests>]
let tests =
  testList "samples" [
    testTheory
        "null and empty arent the same"
        [ ""; null ]
        (fun value ->

            printf "%s" value)
  ]

The the test explorer in Ionide only showed one test:

image

And then when I tried running from the command line, I got

[11:06:04 INF] Found duplicated test names, these names are: [["samples"; "null and empty arent the same"; ""]] <Expecto>

Maybe it's not a typical thing to do in F#, but for comparison If I do the same thing in NUnit with TestCase then it puts the string 'null' in the test name rather than an empty string so that the two tests show up differently: image

So maybe it'd be possible to do something similar here?

Thanks

farlee2121 commented 3 months ago

Thanks for the examples!

It sounds like the problem is that null and empty string are, by default, rendered as the same string so the test name for both cases is the same and the test runner gets mad, thus displaying only one test.

I'd guess the solution here is to introduce a pretty printer that detects special values like this.

That'd probably be changed here

let inline testTheory name cases test =
    let caseToTest case =
        testCase (string case) <| fun () ->
        //        ^^^^^^^

Just conjecture though. I haven't tried anything yet

Numpsy commented 3 months ago

Just conjecture though. I haven't tried anything yet

Just from a quick go with Expectos own tests, it does look like a different string function like

  let inline private stringify (value : 'T) =
      if typeof<'T> = typeof<string> then
        if value = Unchecked.defaultof<'T> then "null" else value.ToString()
      else
        string value

  /// Builds a theory test case
  let inline testTheory name cases test =
    let caseToTest case =
      testCase (stringify case) <| fun () ->
        test case |> ignore
    testList name (cases |> Seq.map caseToTest |> List.ofSeq)

lets these cases pass

    testTheory "odd numbers" [1; 3; 5] <| fun x ->
      Expect.isTrue (x % 2 = 1) "should be odd"

    testTheory "empty strings" [""; null] <| fun x ->
      Expect.isTrue (System.String.IsNullOrEmpty(x)) "should be null or empty"
farlee2121 commented 2 months ago

Moving this from the PR for discoverability and potential continued discussion

While not very elegant, a workaround could be to give each case a data point to differentiate it. I.e. [("case1", [array here]); ("case2", [other array]).

Actually, we could potentially generalize such an approach. Automatically add a case identifier to make sure each case is unique even if the arguments don't have distinct string representations.