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 }
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With