patternModerate
Mock/Stub out filesystem in F# for unit testing
Viewed 0 times
stubmocktestingforfilesystemoutunit
Problem
I'm looking to do some basic verification testing on my functions that write to the filesystem.
I took a hint from here on how to mock out the filesystem using an interface, but I'm kinda bummed on how the
I'm rather new to FP, so any feedback on other design issues are welcome as well.
```
module FileSystem =
open System.IO
type IFileSystemOperations =
abstract member copy: string -> string -> bool -> unit
abstract member delete: string -> unit
abstract member readAllBytes: string -> byte[]
abstract member createDirectory: string -> DirectoryInfo
abstract member directoryExists: string -> bool
type FileSystemOperations () =
interface IFileSystemOperations with
member this.copy source destination overwrite =
File.Copy(source, destination, overwrite)
member this.delete path =
File.Delete path
member this.readAllBytes path =
File.ReadAllBytes path
member this.createDirectory path =
Directory.CreateDirectory path
member this.directoryExists path =
let fileInfo = FileInfo path
fileInfo.Directory.Exists
module FileMover =
open FileSystem
let private ensureDirectoryExists (fileSystemsOperations:IFileSystemOperations) destination =
let directoryExists = fileSystemsOperations.directoryExists destination
if not (directoryExists) then
fileSystemsOperations.createDirectory destination |> ignore
let private compareFiles (fileSystemsOperations:IFileSystemOperations) moveRequest =
let sourceStream = fileSystemsOperations.readAllBytes moveRequest.Source
let destinationStream = fileSystemsOperations.readAllBytes moveRequest.Destination
sourceStream = destinationSt
I took a hint from here on how to mock out the filesystem using an interface, but I'm kinda bummed on how the
FileSystemOperations type needs to be passed along through every function. Any ideas how to improve this, or where I went wrong?I'm rather new to FP, so any feedback on other design issues are welcome as well.
```
module FileSystem =
open System.IO
type IFileSystemOperations =
abstract member copy: string -> string -> bool -> unit
abstract member delete: string -> unit
abstract member readAllBytes: string -> byte[]
abstract member createDirectory: string -> DirectoryInfo
abstract member directoryExists: string -> bool
type FileSystemOperations () =
interface IFileSystemOperations with
member this.copy source destination overwrite =
File.Copy(source, destination, overwrite)
member this.delete path =
File.Delete path
member this.readAllBytes path =
File.ReadAllBytes path
member this.createDirectory path =
Directory.CreateDirectory path
member this.directoryExists path =
let fileInfo = FileInfo path
fileInfo.Directory.Exists
module FileMover =
open FileSystem
let private ensureDirectoryExists (fileSystemsOperations:IFileSystemOperations) destination =
let directoryExists = fileSystemsOperations.directoryExists destination
if not (directoryExists) then
fileSystemsOperations.createDirectory destination |> ignore
let private compareFiles (fileSystemsOperations:IFileSystemOperations) moveRequest =
let sourceStream = fileSystemsOperations.readAllBytes moveRequest.Source
let destinationStream = fileSystemsOperations.readAllBytes moveRequest.Destination
sourceStream = destinationSt
Solution
Even though the SOLID principles are known to be principles related to Object-Oriented Design, I'd still take a cue from the Dependency Inversion Principle. As APPP states it (ch. 11): "clients [...] own the abstract interfaces".
You can apply this principle in FP as well, in the sense that you can start by defining the overall behaviour of the function you're interested in, and then see what happens.
As an example, I'd approach the
Now, the above version doesn't compile, because
Viola: now you have a higher-order function; it's type is
No interfaces are required.
This is already intriguing for a couple of reasons:
You can still 'implement'
Notice that the
The partially applied
While I think I'll leave the rest of the functions as an exercise, I'd like to return to the thought-provoking signature of the
There's nothing here about files or directories, so a more generic name might be in place. It looks like the Tester-Doer idiom, so we could call it
One of the great benefits of making the interface more generic is that you don't need to involve the file system at all for testing. Instead, you can test this function using integers, strings, or some other 'easy' values. If the logic is unconstrained generic and works for numbers and strings, it'll also work for directories and files.
You can apply this principle in FP as well, in the sense that you can start by defining the overall behaviour of the function you're interested in, and then see what happens.
As an example, I'd approach the
ensureDirectoryExists function like this:let private ensureDirectoryExists destination =
let directoryExists = dirExists destination
if not directoryExists then
createDir destinationNow, the above version doesn't compile, because
dirExists and createDir aren't defined. However, that's easy to fix: just promote them to function arguments:let private ensureDirectoryExists dirExists createDir destination =
let directoryExists = dirExists destination
if not directoryExists then
createDir destinationViola: now you have a higher-order function; it's type is
('a -> bool) -> ('a -> unit) -> 'a -> unitNo interfaces are required.
This is already intriguing for a couple of reasons:
- It's no longer about files or directories:
'acould bestringvalues containing file names or directory names, but it could also beDirectoryInfoor something not at all related to files.
- Although it looks complex, it only involves two types:
'aandunit. That should make us pay attention as well.
- You can use partial application to apply the
ensureDirectoryExistswith the two first arguments, and then use this partially applied function to test any number of different directories.
You can still 'implement'
dirExists and createDir, and apply them like this:open System.IO
let directoryExists path =
let fileInfo = FileInfo path
fileInfo.Directory.Exists
let ensureDirectoryExists' =
ensureDirectoryExists directoryExists (Directory.CreateDirectory >> ignore)Notice that the
createDir 'implementation' is so simple that I chose to inline it. I could also have inlined the dirExists 'implementation' with (fun path -> (FileInfo path).Directory.Exists), but I chose to do this as a separate function in order to illustrate the various options available.The partially applied
ensureDirectoryExists' function has this simple function signature: (string -> unit), which is comparable to the OP version of the function.While I think I'll leave the rest of the functions as an exercise, I'd like to return to the thought-provoking signature of the
ensureDirectoryExists function:('a -> bool) -> ('a -> unit) -> 'a -> unitThere's nothing here about files or directories, so a more generic name might be in place. It looks like the Tester-Doer idiom, so we could call it
testDo instead.One of the great benefits of making the interface more generic is that you don't need to involve the file system at all for testing. Instead, you can test this function using integers, strings, or some other 'easy' values. If the logic is unconstrained generic and works for numbers and strings, it'll also work for directories and files.
Code Snippets
let private ensureDirectoryExists destination =
let directoryExists = dirExists destination
if not directoryExists then
createDir destinationlet private ensureDirectoryExists dirExists createDir destination =
let directoryExists = dirExists destination
if not directoryExists then
createDir destination('a -> bool) -> ('a -> unit) -> 'a -> unitopen System.IO
let directoryExists path =
let fileInfo = FileInfo path
fileInfo.Directory.Exists
let ensureDirectoryExists' =
ensureDirectoryExists directoryExists (Directory.CreateDirectory >> ignore)('a -> bool) -> ('a -> unit) -> 'a -> unitContext
StackExchange Code Review Q#99271, answer score: 11
Revisions (0)
No revisions yet.