Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# UnitTesting function with side effect

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.

like image 703
Cedric Royer-Bertrand Avatar asked Jul 30 '17 13:07

Cedric Royer-Bertrand


1 Answers

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"
like image 120
Fyodor Soikin Avatar answered Sep 27 '22 23:09

Fyodor Soikin