HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Unit testing F# code without using an existing test library (MSTest, xUnit, etc.)

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
mstestwithoutexistingtestingusingtestlibrarycodexunitunit

Problem

I know I can probably use MSTest (I'm under Linux without an IDE so maybe not?) or another unit testing library, but for a small project I decided to write my own unit tests without resorting to a framework (for fun and for learning).

Right now, I have only one type of assert and one test but I would like a code review to improve my code now to make sure I am on the right track.

module Tests

// modules under test
open HeightMap
open MidpointDisplacement

// assert functions
let assertAreEqual expected actual =
    printf "... "
    if expected <> actual then printfn "%s" ("Test failed, expected " + expected.ToString() + ", actual " + actual.ToString())  
    else printfn "Test passed"

// tests
let testNewHeightMapReturnZeroInitializedHm () = 
    let hm = newHeightMap 5
    let result = hm.Map |> Array.sum
    assertAreEqual 0.0 result

// tests included in run
let testsToRun = 
    [
        ("test newHeightMap will return a 0 initialized height map",
         testNewHeightMapReturnZeroInitializedHm)
    ]

// test runner
let runSingleTest test = 
    let testName, testFunction = test
    printf "%s" testName    
    testFunction()

let runTests =
    testsToRun |> List.map runSingleTest |> ignore
    printfn "%s" "Ran all tests."


One thing that bugs me is that I there is some redundancy by specifying the test function and then including it in a list inside a tuple.

Solution

Overall this looks ok, just a couple nitpicks:

-
In assertAreEqual, you're using string concatenation and then passing the resulting string to formatting function. You could use the formatting function to begin with:

if expected <> actual then 
    printfn "Test failed, expected %A, actual %A" expected actual  
else 
    printfn "Test passed"


-
Parentheses around tuples are not required, so:

let testsToRun = 
[
    "test newHeightMap will return a 0 initialized height map",
    testNewHeightMapReturnZeroInitializedHm
]


-
Function parameters are actually patterns, so you can destructure the tuple right there, instead of on a separate line:

let runSingleTest (testName, testFunction) = 
    ...


-
As a general rule, if you have to use ignore, it means that there is something wrong somewhere. ignore is a hack, a way to squeeze a round peg in a square hole. In your particular case, you're mapping over a list solely for the side-effects. To do this, use List.iter, which returns unit, so no need for ignore.

testsToRun |> List.iter runSingleTest


-
The pervasive use of side-effecting computations (i.e.
printfn etc.) sets off my spidey sense: this makes your code inflexible (e.g. what if you want to have different reporting formats - one for console run, one for CI server, etc.).

You should structure your code in such a way that it doesn't produce side effects as it runs, but instead produces a value that describes the run, and which can later be used (or not used) to produce the effects. For example, in your case, rather than printing out errors, I would return a list of them, and have the consumer decide what to do with that list. Of course, this would mean significant restructuring of your whole program, so I won't give a code example.

-
Finally, "parametrize all the things!" Try to take stuff as parameters as much as possible, rather than relying on stuff existing in the environment. For example, the
runTests` function should really take the list of tests as parameter.

-
As far as the thing that bugs you, there is an easy solution for it: you don't have to name your functions, you can declare them inline as lambda-expressions:

let testsToRun = 
    [
        "test newHeightMap will return a 0 initialized height map",
        fun() -> 
            let hm = newHeightMap 5
            let result = hm.Map |> Array.sum
            assertAreEqual 0.0 result
    ]


-
Or, alternatively, you can give a name to the pair of name+function, and then include that pair in the list:

let theTest = "test newHeightMap will return a 0 initialized height map", fun() -> 
    let hm = newHeightMap 5
    let result = hm.Map |> Array.sum
    assertAreEqual 0.0 result

let testsToRun = [ theTest ]

Code Snippets

if expected <> actual then 
    printfn "Test failed, expected %A, actual %A" expected actual  
else 
    printfn "Test passed"
let testsToRun = 
[
    "test newHeightMap will return a 0 initialized height map",
    testNewHeightMapReturnZeroInitializedHm
]
let runSingleTest (testName, testFunction) = 
    ...
testsToRun |> List.iter runSingleTest
let testsToRun = 
    [
        "test newHeightMap will return a 0 initialized height map",
        fun() -> 
            let hm = newHeightMap 5
            let result = hm.Map |> Array.sum
            assertAreEqual 0.0 result
    ]

Context

StackExchange Code Review Q#121503, answer score: 7

Revisions (0)

No revisions yet.