Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do functional programmers test functions that return a unit?

Tags:

f#

How do functional programmers test functions that return a unit?

In my case, I believe I need to unit test an interface to this function:

let logToFile (filePath:string) (formatf : 'data -> string) data =
    use file = new System.IO.StreamWriter(filePath)
    file.WriteLine(formatf data)
    data

What is the recommended approach when I'm unit testing a function with I/O?

In OOP, I believe a Test Spy can be leveraged.

Does the Test Spy pattern translate to functional programming?

My client looks something like this:

[<Test>]
let ``log purchase``() =
    [OneDollarBill] |> select Pepsi
                    |> logToFile "myFile.txt" (sprintf "%A")
                    |> should equal ??? // IDK

My domain is the following:

module Machine

type Deposit =
    | Nickel
    | Dime
    | Quarter
    | OneDollarBill
    | FiveDollarBill

type Selection =
    | Pepsi
    | Coke
    | Sprite
    | MountainDew

type Attempt = {  
    Price:decimal
    Need:decimal
}

type Transaction = {
    Purchased:Selection  
    Price:decimal
    Deposited:Deposit list
}

type RequestResult =
    | Granted of Transaction
    | Denied of Attempt

(* Functions *)
open System

let insert coin balance = coin::balance
let refund coins = coins

let priceOf = function
    | Pepsi
    | Coke
    | Sprite
    | MountainDew  -> 1.00m

let valueOf = function
    | Nickel         -> 0.05m
    | Dime           -> 0.10m
    | Quarter        -> 0.25m
    | OneDollarBill  -> 1.00m
    | FiveDollarBill -> 5.00m

let totalValue coins =
    (0.00m, coins) ||> List.fold (fun acc coin -> acc + valueOf coin)

let logToFile (filePath:string) (formatf : 'data -> string) data =
    let message = formatf data
    use file = new System.IO.StreamWriter(filePath)
    file.WriteLine(message)
    data

let select item deposited =
    if totalValue deposited >= priceOf item

    then Granted { Purchased=item
                   Deposited=deposited
                   Price = priceOf item }

    else Denied { Price=priceOf item; 
                  Need=priceOf item - totalValue deposited }
like image 481
Scott Nimrod Avatar asked Jul 19 '16 15:07

Scott Nimrod


2 Answers

Do not see this as an authoritative answer, because I'm not an expert on testing, but my answer to this question would be that, in a perfect world, you cannot and do not need to test unit-returning functions.

Ideally, you would structure your code so that it is composed from some IO to read data, transformations encoding all the logic and some IO to save the data:

read
|> someLogic
|> someMoreLogic
|> write

The idea is that all your important things are in someLogic and someMoreLogic and that read and write are completely trivial - they read file as string or sequence of lines. This is trivial enough that you do not need to test it (now, you could possibly test the actual file writing by reading the file back again, but that's when you want to test the file IO rather than any logic that you wrote).

This is where you would use a mock in OO, but since you have a nice functional structure, you would now write:

testData
|> someLogic
|> someMoreLogic
|> shouldEqual expectedResult

Now, in reality, the world is not always that nice and something like a spy operation ends up being useful - perhaps because you are interoperating with a world that is not purely functional.

Phil Trelford has a nice and very simple Recorder that lets you record calls to a function and check that it has been called with the expected inputs - and this is something I've found useful a number of times (and it is simple enough that you do not really need a framework).

like image 163
Tomas Petricek Avatar answered Sep 27 '22 20:09

Tomas Petricek


Obviously, you could use a mock as you would in imperative code as long as the unit of code takes its dependencies as a parameter.

But, for another approach, I found this talk really interesting Mocks & stubs by Ken Scambler. As far as I recall the general argument was that you should avoid using mocks by keeping all functions as pure as possible, making them data-in-data-out. At the very edges of your program, you would have some very simple functions that perform the important side-effects. These are so simple that they don't even need testing.

The function you provided is simple enough to fall into that category. Testing it with a mock or similar would just involve ensuring that certain methods are called, not that the side-effect occurred. Such a test isn't meaningful and doesn't add any value over the code itself, while still adding a maintenance burden. It's better to test the side-effect part with an integration test or end-to-end test that actually looks at the file that was written.

Another good talk on the subject is Boundaries by Gary Bernhardt which Discusses the concept of Functional Core, Imperative Shell.

like image 27
TheQuickBrownFox Avatar answered Sep 27 '22 22:09

TheQuickBrownFox