F# UnitTesting function with side effect
Asked Answered
J

2

7

I am C# dev that has just starting to learn F# and I have a few questions about unit testing. Let's say I want to the following code:

let input () = Console.In.ReadLine()

type MyType= {Name:string; Coordinate:Coordinate}

let readMyType = 
   input().Split(';') 
   |> fun x -> {Name=x.[1]; Coordinate = {
   Longitude = float(x.[4].Replace(",",".")) 
   Latitude =float(x.[5].Replace(",","."))
   }}

As you can notice, there are a few points to take in consideration:

  • readMyType is calling input() with has a side effect.
  • readMyType assume many thing on the string read (contains ';' at least 6 columns, some columns are float with ',')

I think the way of doing this would be to:

  • inject the input() func as parameter
  • try to test what we are getting (pattern matching?)
  • Using NUnit as explained here

To be honest I'm just struggling to find an example that is showing me this, in order to learn the syntax and other best practices in F#. So if you could show me the path that would be very great.

Thanks in advance.

Juieta answered 30/7, 2017 at 13:19 Comment(0)
M
9

First, your function is not really a function. It's a value. The distinction between functions and values is syntactic: if you have any parameters, you're a function; otherwise - you're a value. The consequence of this distinction is very important in presence of side effects: values are computed only once, during initialization, and then never change, while functions are executed every time you call them.

For your specific example, this means that the following program:

let main _ =
   readMyType
   readMyType
   readMyType
   0

will ask the user for only one input, not three. Because readMyType is a value, it gets initialized once, at program start, and any subsequent reference to it just gets the pre-computed value, but doesn't execute the code over again.

Second, - yes, you're right: in order to test this function, you'd need to inject the input function as a parameter:

let readMyType (input: unit -> string) = 
   input().Split(';') 
   |> fun x -> {Name=x.[1]; Coordinate = {
   Longitude = float(x.[4].Replace(",",".")) 
   Latitude =float(x.[5].Replace(",","."))
   }}

and then have the tests supply different inputs and check different outcomes:

let [<Test>] ``Successfully parses correctly formatted string``() = 
   let input() = "foo;the_name;bar;baz;1,23;4,56"
   let result = readMyType input
   result |> should equal { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } }

let [<Test>] ``Fails when the string does not have enough parts``() = 
   let input() = "foo"
   (fun () -> readMyType input) |> shouldFail

// etc.

Put these tests in a separate project, add reference to your main project, then add test runner to your build script.


UPDATE
From your comments, I got the impression that you were seeking not only to test the function as it is (which follows from your original question), but also asking for advice on improving the function itself, so as to make it more safe and usable.

Yes, it is definitely better to check error conditions within the function, and return appropriate result. Unlike C#, however, it is usually better to avoid exceptions as control flow mechanism. Exceptions are for exceptional situations. For such situations that you would have never expected. That is why they are exceptions. But since the whole point of your function is parsing input, it stands to reason that invalid input is one of the normal conditions for it.

In F#, instead of throwing exceptions, you would usually return a result that indicates whether the operation was successful. For your function, the following type seems appropriate:

type ErrorMessage = string
type ParseResult = Success of MyType | Error of ErrorMessage

And then modify the function accordingly:

let parseMyType (input: string) =
    let parts = input.Split [|';'|]
    if parts.Length < 6 
    then 
       Error "Not enough parts"
    else
       Success 
         { Name = parts.[0] 
           Coordinate = { Longitude = float(parts.[4].Replace(',','.')
                          Latitude = float(parts.[5].Replace(',','.') } 
         }

This function will return us either MyType wrapped in Success or an error message wrapped in Error, and we can check this in tests:

let [<Test>] ``Successfully parses correctly formatted string``() = 
   let input() = "foo;the_name;bar;baz;1,23;4,56"
   let result = readMyType input
   result |> should equal (Success { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } })

let [<Test>] ``Fails when the string does not have enough parts``() = 
   let input() = "foo"
   let result = readMyType input
   result |> should equal (Error "Not enough parts)

Note that, even though the code now checks for enough parts in the string, there are still other possible error conditions: for example, parts.[4] may be not a valid number.

I am not going to expand on this further, as that will make the answer way too long. I will only stop to mention two points:

  1. Unlike C#, verifying all error conditions does not have to end up as a pyramid of doom. Validations can be nicely combined in a linear-looking way (see example below).
  2. The F# 4.1 standard library already provides a type similar to ParseResult above, named Result<'t, 'e>.

For more on this approach, check out this wonderful post (and don't forget to explore all links from it, especially the video).

And here, I will leave you with an example of what your function could look like with full validation of everything (keep in mind though that this is not the cleanest version still):

let parseFloat (s: string) = 
    match System.Double.TryParse (s.Replace(',','.')) with
    | true, x -> Ok x
    | false, _ -> Error ("Not a number: " + s)

let split n (s:string)  =
    let parts = s.Split [|';'|]
    if parts.Length < n then Error "Not enough parts"
    else Ok parts

let parseMyType input =
    input |> split 6 |> Result.bind (fun parts ->
    parseFloat parts.[4] |> Result.bind (fun lgt ->
    parseFloat parts.[5] |> Result.bind (fun lat ->
    Ok { Name = parts.[1]; Coordinate = { Longitude = lgt; Latitude = lat } } )))

Usage:

> parseMyType "foo;name;bar;baz;1,23;4,56"
val it : Result<MyType,string> = Ok {Name = "name";
                                     Coordinate = {Longitude = 1.23;
                                                   Latitude = 4.56;};}

> parseMyType "foo"
val it : Result<MyType,string> = Error "Not enough parts"

> parseMyType "foo;name;bar;baz;badnumber;4,56"
val it : Result<MyType,string> = Error "Not a number: badnumber"
Manysided answered 30/7, 2017 at 13:45 Comment(2)
Thanks for this detailed answer and the explanation on function and value, that is really helpful. By reading your answer It seems that you're not adding any check in the code itself but just checking it's failing in the unit test, why that? (I mean in C# I would have add check and throws an exception then check it in a unit test. Seems more code but I'm used to TDD approach)Juieta
Thanks for the update, the article on Railway oriented programming is very interesting.Juieta
Q
1

This is a little follow-up to the excellent answer of @FyodorSoikin trying to explore the suggestion

keep in mind though that this is not the cleanest version still

Making the ParseResult generic

type ParseResult<'a> = Success of 'a | Error of ErrorMessage
type ResultType = ParseResult<Defibrillator> // see the Test Cases

we can define a builder

type Builder() =
    member x.Bind(r :ParseResult<'a>, func : ('a -> ParseResult<'b>)) = 
        match r with
        | Success m -> func m
        | Error w -> Error w 
    member x.Return(value) = Success value
let builder = Builder()

so we get a concise notation:

let parse input =
    builder {
       let! parts = input |> split 6
       let! lgt = parts.[4] |> parseFloat 
       let! lat = parts.[5] |> parseFloat 
       return { Name = parts.[1]; Coordinate = { Longitude = lgt; Latitude = lat } }
    }

Test Cases

Tests are always fundamental

let [<Test>] ``3. Successfully parses correctly formatted string``() = 
   let input = "foo;the_name;bar;baz;1,23;4,56"
   let result = parse input
   result |> should equal (ResultType.Success { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } })

let [<Test>] ``3. Fails when the string does not have enough parts``() = 
   let input = "foo"
   let result = parse input
   result |> should equal (ResultType.Error "Not enough parts")

let [<Test>] ``3. Fails when the string does not contain a number``() = 
   let input = "foo;name;bar;baz;badnumber;4,56"
   let result = parse input
   result |> should equal  (ResultType.Error "Not a number: badnumber")

Notice the usage of a specific ParseResult from the generic one.

minor note

Double.TryParse is just enough in the following

let parseFloat (s: string) = 
    match Double.TryParse s with
    | true, x -> Success x
    | false, _ -> Error ("Not a number: " + s)
Quinine answered 30/7, 2017 at 22:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.