From 0acf162b974d5c6cb9ba24b38dff7c561fc0715c Mon Sep 17 00:00:00 2001 From: Henrik Feldt Date: Mon, 26 Dec 2016 00:38:00 +0100 Subject: [PATCH] Fixes #37 ref #33, #34, improve exit msg, fix internal tests --- .vscode/launch.json | 7 +- .vscode/tasks.json | 5 +- Expecto.Sample/Expecto.Sample.fs | 35 ++++- Expecto.Tests/Main.fs | 2 +- Expecto.Tests/Prelude.fs | 227 +++++++++++++++---------------- Expecto.Tests/Tests.fs | 144 +++++++++----------- Expecto/Expect.fs | 88 +++++++----- Expecto/Expecto.fs | 56 ++++++-- README.md | 14 ++ 9 files changed, 326 insertions(+), 252 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ce465e9f..5e71e0ee 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,13 +2,12 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug Tests", + "name": "Debug Sample", "type": "mono", "request": "launch", - "program": "${workspaceRoot}/Expecto.Tests/bin/Release/Expecto.Tests.exe", - "args": ["--list-tests"], + "program": "${workspaceRoot}/Expecto.Sample/bin/Debug/Expecto.Sample.exe", + "args": ["--debug"], "cwd": "${workspaceRoot}", - "preLaunchTask": "compile", "runtimeExecutable": null, "env": {}, "externalConsole": false diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 56d97435..87503483 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,7 +2,7 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "0.1.0", - "command": "rake", + "command": "bundle", "isShellCommand": true, "args": [], "showOutput": "always", @@ -10,7 +10,8 @@ { "taskName": "compile", "args": [ - "compile" + "exec", + "rake" ], "isBuildCommand": true } diff --git a/Expecto.Sample/Expecto.Sample.fs b/Expecto.Sample/Expecto.Sample.fs index f652bcae..199db982 100644 --- a/Expecto.Sample/Expecto.Sample.fs +++ b/Expecto.Sample/Expecto.Sample.fs @@ -5,13 +5,42 @@ open Expecto [] let tests = testList "samples" [ - testCase "universe exists" <| fun _ -> + testCase "universe exists (╭ರᴥ•́)" <| fun _ -> let subject = true Expect.isTrue subject "I compute, therefore I am." - testCase "should fail" <| fun _ -> + testCase "when true is not (should fail)" <| fun _ -> let subject = false - Expect.isTrue subject "I should fail because the subject is false." + Expect.isTrue subject "I should fail because the subject is false" + + testCase "I'm skipped (should skip)" <| fun _ -> + Tests.skiptest "Yup, waiting for a sunny day..." + + testCase "I'm always fail (should fail)" <| fun _ -> + Tests.failtest "This was expected..." + + testCase "contains things" <| fun _ -> + Expect.containsAll [| 2; 3; 4 |] [| 2; 4 |] + "This is the case; {2,3,4} contains {2,4}" + + testCase "contains things (should fail)" <| fun _ -> + Expect.containsAll [| 2; 3; 4 |] [| 2; 4; 1 |] + "Expecting we have one (1) in there" + + testCase "Sometimes I want to ༼ノಠل͟ಠ༽ノ ︵ ┻━┻" <| fun _ -> + Expect.equal "\ + abc\n\ + dëf\ + " + "abc\ndef" + "These should equal" + + test "I am (should fail)" { + "╰〳 ಠ 益 ಠೃ 〵╯" |> Expect.equal true false + } + + testCase "You know exns" <| fun _ -> + failwith "unhandled exception from test code" ] [] diff --git a/Expecto.Tests/Main.fs b/Expecto.Tests/Main.fs index 5c0d62b1..253b5c72 100644 --- a/Expecto.Tests/Main.fs +++ b/Expecto.Tests/Main.fs @@ -4,4 +4,4 @@ open Expecto [] let main args = - runTestsInAssembly defaultConfig args + runTestsInAssembly defaultConfig args \ No newline at end of file diff --git a/Expecto.Tests/Prelude.fs b/Expecto.Tests/Prelude.fs index 59502b98..ea777d69 100644 --- a/Expecto.Tests/Prelude.fs +++ b/Expecto.Tests/Prelude.fs @@ -1,114 +1,113 @@ -namespace Expecto -#nowarn "44" - -open System -open System.Text.RegularExpressions -open FSharpx - -module Seq = - let (|Empty|Cons|) l = - if Seq.isEmpty l - then Empty - else Cons(Seq.head l, Seq.skip 1 l) - - let (|One|_|) l = - match Seq.toList l with - | [x] -> Some x - | _ -> None - - let (|Two|_|) l = - match Seq.toList l with - | [x;y] -> Some(x,y) - | _ -> None - -module String = - let internal nullBool2 f a b = - if a = null && a = null then - true - elif a = null || b = null then - false - else - f b a - - let internal nullOption2 f a b = - nullBool2 f a b |> Option.ofBool - - let (|StartsWith|_|) = - nullOption2 (fun (s: string) -> s.StartsWith) - - let (|Contains|_|) = - nullOption2 (fun (s: string) -> s.Contains) - -[] -module TestHelpers = - open Expecto - open Expecto.Impl - - let evalSilent = eval TestPrinters.Default Seq.map - - let inline assertTestFails test = - let test = TestCase test - match evalSilent test with - | [{ TestRunResult.result = TestResult.Failed _ }] -> () - | x -> failtestf "Should have failed, but was %A" x - - let inline assertTestFailsWithMsg (msg : string) test = - let normalize str = Regex.Replace(msg, @"\s", "") - - let test = TestCase test - match evalSilent test with - | [{ TestRunResult.result = TestResult.Failed x }] when String.Equals(normalize x, normalize msg) -> () - | [{ TestRunResult.result = TestResult.Failed x }] -> failtestf "Shold have failed with message: \"%s\" but failed with \"%s\"" msg x - | x -> failtestf "Should have failed, but was %A" x - - - open FsCheck - - let genLimitedTimeSpan = - lazy ( - Arb.generate - |> Gen.suchThat (fun t -> t.Days = 0) - ) - - let genTestResultCounts = - lazy ( - gen { - let! passed = Arb.generate - let! ignored = Arb.generate - let! failed = Arb.generate - let! errored = Arb.generate - let! duration = genLimitedTimeSpan.Value - return - { TestResultSummary.passed = passed - ignored = ignored - failed = failed - errored = errored - duration = duration } - } - ) - - let shrinkTestResultCounts (c: TestResultSummary) : TestResultSummary seq = - seq { - for passed in Arb.shrink c.passed do - for ignored in Arb.shrink c.ignored do - for failed in Arb.shrink c.failed do - for errored in Arb.shrink c.errored do - for duration in Arb.shrink c.duration -> - { - TestResultSummary.passed = passed - ignored = ignored - failed = failed - errored = errored - duration = duration - } - } - - let arbTestResultCounts = - lazy ( - Arb.fromGenShrink(genTestResultCounts.Value, shrinkTestResultCounts) - ) - - let twoTestResultCounts = - lazy ( - Gen.two arbTestResultCounts.Value.Generator |> Arb.fromGen - ) +namespace Expecto +#nowarn "44" + +open System +open System.Text.RegularExpressions +open FSharpx + +module Seq = + let (|Empty|Cons|) l = + if Seq.isEmpty l + then Empty + else Cons(Seq.head l, Seq.skip 1 l) + + let (|One|_|) l = + match Seq.toList l with + | [x] -> Some x + | _ -> None + + let (|Two|_|) l = + match Seq.toList l with + | [x;y] -> Some(x,y) + | _ -> None + +module String = + let internal nullBool2 f a b = + if a = null && a = null then + true + elif a = null || b = null then + false + else + f b a + + let internal nullOption2 f a b = + nullBool2 f a b |> Option.ofBool + + let (|StartsWith|_|) = + nullOption2 (fun (s: string) -> s.StartsWith) + + let (|Contains|_|) = + nullOption2 (fun (s: string) -> s.Contains) + +[] +module TestHelpers = + open Expecto + open Expecto.Impl + + let evalSilent = eval TestPrinters.silent Seq.map + + let inline assertTestFails test = + let test = TestCase test + match evalSilent test with + | [{ TestRunResult.result = TestResult.Failed _ }] -> () + | x -> failtestf "Should have failed, but was %A" x + + let inline assertTestFailsWithMsg (msg : string) test = + let test = TestCase test + match evalSilent test with + | [{ TestRunResult.result = TestResult.Failed x }] -> + let trimmed = x.Trim('\n') + Expect.equal trimmed msg "Test failure strings should equal" + | x -> + failtestf "Should have failed, but was %A" x + + open FsCheck + + let genLimitedTimeSpan = + lazy ( + Arb.generate + |> Gen.suchThat (fun t -> t.Days = 0) + ) + + let genTestResultCounts = + lazy ( + gen { + let! passed = Arb.generate + let! ignored = Arb.generate + let! failed = Arb.generate + let! errored = Arb.generate + let! duration = genLimitedTimeSpan.Value + return + { TestResultSummary.passed = passed + ignored = ignored + failed = failed + errored = errored + duration = duration } + } + ) + + let shrinkTestResultCounts (c: TestResultSummary) : TestResultSummary seq = + seq { + for passed in Arb.shrink c.passed do + for ignored in Arb.shrink c.ignored do + for failed in Arb.shrink c.failed do + for errored in Arb.shrink c.errored do + for duration in Arb.shrink c.duration -> + { + TestResultSummary.passed = passed + ignored = ignored + failed = failed + errored = errored + duration = duration + } + } + + let arbTestResultCounts = + lazy ( + Arb.fromGenShrink(genTestResultCounts.Value, shrinkTestResultCounts) + ) + + let twoTestResultCounts = + lazy ( + Gen.two arbTestResultCounts.Value.Generator |> Arb.fromGen + ) diff --git a/Expecto.Tests/Tests.fs b/Expecto.Tests/Tests.fs index b96b58be..45b6d2fc 100644 --- a/Expecto.Tests/Tests.fs +++ b/Expecto.Tests/Tests.fs @@ -40,54 +40,55 @@ let tests = Expect.equal "Test string" "Test string" "Test string" } - test "fail - different length, shorter" { + test "different length, actual is shorter" { let format = "Failing - string with different length" - let fstText = "Test" - let sndText = "Tes2" - let diffString = " ↑" - let test () = Expect.equal fstText sndText format - let msg = sprintf "%s. - Expected string to equal - %A - %s - The string differ at index %d. - %A - %s - Sequence actual shorter than expected, at pos %i for expected item %A." format fstText diffString 4 sndText diffString 4 '2' + let actual = "Test" + let expected = "Test2" + let diffString = " ↑" + let test () = Expect.equal actual expected format + let msg = + sprintf "%s. + Expected string to equal: + %A + %s + The string differs at index %d. + %A + %s + String `actual` was shorter than expected, at pos %i for expected item %A." + format expected diffString 4 actual diffString 4 '2' assertTestFailsWithMsg msg (test, Normal) } - test "fail - different length, longer" { + test "different length, actual is longer" { let format = "Failing - string with different length" - let fstText = "Test" - let sndText = "Tes2" - let diffString = " ↑" - let test () = Expect.equal sndText fstText format - let msg = sprintf "%s. - Expected string to equal - %A - %s - The string differ at index %d. - %A - %s - Sequence actual longer than expected, at pos %i found item %A." format fstText diffString 4 sndText diffString 4 '2' + let test () = Expect.equal "Test2" "Test" format + let msg = + sprintf """%s. + Expected string to equal: + "Test" + ↑ + The string differs at index 4. + "Test2" + ↑ + String `actual` was longer than expected, at pos 4 found item '2'.""" + format assertTestFailsWithMsg msg (test, Normal) } test "fail - different content" { let format = "Failing - string with different content" - let fstText = "Test" - let sndText = "Tes2" + let test () = Expect.equal "Test" "Tes2" format let diffString = " ↑" - let msg = sprintf "%s. - Expected string to equal - %A - %s - The string differ at index %d. - %A - %s - Sequence does not match at position %i. Expected char: %A, but got %A." format fstText diffString 4 sndText diffString 3 't' '2' - let test () = Expect.equal sndText fstText format + let msg = + sprintf """%s. + Expected string to equal: + "Tes2" + ↑ + The string differs at index 3. + "Test" + ↑ + String does not match at position 3. Expected char: '2', but got 't'.""" + format assertTestFailsWithMsg msg (test, Normal) } ] @@ -158,8 +159,8 @@ let tests = ] [] -let timeouts = - testList "timeout" [ +let expecto = + testList "expecto" [ testList "Setup & teardown 2" [ // just demoing how you can use a higher-order function as setup/teardown let tests = [ @@ -431,50 +432,31 @@ let timeouts = assertTestFails (test, Normal) ] - testList "contains all comparison" [ - test "sequence contains all" { - Expect.containsAll [|21;37|] [|21;37|] "test sequence" - } - - test "sequence contains all in different order" { - Expect.containsAll [|21;37|] [|37;21|] "test sequence" - } + testList "#containsAll" [ + testCase "identical sequence" <| fun _ -> + Expect.containsAll [|21;37|] [|21;37|] "Identical" - test "fail - actual sequence contains more elements" { - let format = "Failing - sequence does not contain proper elements" - let fstSeq = [|2;1;3|] - let sndSeq = [|1;3|] - let test () = Expect.containsAll fstSeq sndSeq format - let msg = sprintf "%s. - Sequence does not contains all elements - Should contains: - %A - But contains: - %A - Missing values: - %A" - format fstSeq sndSeq ([|2|] |> Seq.toList) - assertTestFailsWithMsg msg (test, Normal) - } + testCase "sequence contains all in different order" <| fun _ -> + Expect.containsAll [|21;37|] [|37;21|] + "Same elements in different order" - test "fail - expected sequence contains more elements" { - let format = "Failing - sequence does not contain proper elements" - let fstSeq = [|2;1;3|] - let sndSeq = [|1;3;2;7|] - let test () = Expect.containsAll fstSeq sndSeq format - let msg = sprintf "%s. - Sequence does not contains all elements - Should contains: - %A - But contains: - %A - Missing values: - %A - Should not contains these values but contains: - %A" - format fstSeq sndSeq ([||] |> Seq.toList) ([|7|] |> Seq.toList) + testCase "sequence contains everything expected" <| fun _ -> + let format = "Sequence should contain one and five" + let test () = + Expect.containsAll [|2; 1; 3|] [| 1; 5 |] format + let msg = + sprintf "%s. + Sequence `actual` does not contain all `expected` elements. + All elements in `actual`: + {1, 2, 3} + All elements in `expected`: + {1, 5} + Missing elements from `actual`: + {5} + Extra elements in `actual`: + {2, 3}" + format assertTestFailsWithMsg msg (test, Normal) - } ] testList "sequence equal" [ @@ -545,4 +527,4 @@ let timeouts = let compexpTag = TestResult.tag compexp.[0].result Expect.equal normalTag compexpTag "result" ] - ] + ] \ No newline at end of file diff --git a/Expecto/Expect.fs b/Expecto/Expect.fs index cd53e64d..3baac33e 100644 --- a/Expecto/Expect.fs +++ b/Expecto/Expect.fs @@ -5,6 +5,7 @@ module Expecto.Expect () open System +type HSet<'a> = System.Collections.Generic.HashSet<'a> /// Expects f to throw an exception. let throws f format = @@ -128,13 +129,13 @@ let inline equal (actual : 'a) (expected : 'a) format = use ai = a.GetEnumerator() use ei = e.GetEnumerator() let mutable i = 0 - let baseMsg errorIndex = + let baseMsg errorIndex = let diffString = new String(' ', errorIndex + 1) + "↑" sprintf "%s. Expected string to equal: %A %s - The string differ at index %d. + The string differs at index %d. %A %s" format expected diffString errorIndex actual diffString @@ -144,16 +145,16 @@ let inline equal (actual : 'a) (expected : 'a) format = else Tests.failtestf "%s String does not match at position %i. Expected char: %A, but got %A." - (i |> baseMsg) i (ei.Current) (ai.Current) + (baseMsg i) i ei.Current ai.Current else Tests.failtestf "%s - String actual shorter than expected, at pos %i for expected item %A." - (i |> baseMsg) i (ei.Current) + String `actual` was shorter than expected, at pos %i for expected item %A." + (baseMsg i) i ei.Current i <- i + 1 if ai.MoveNext() then Tests.failtestf "%s - String actual longer than expected, at pos %i found item %A." - (i |> baseMsg) i (ai.Current) + String `actual` was longer than expected, at pos %i found item %A." + (baseMsg i) i (ai.Current) | _, _ -> if actual <> expected then Tests.failtestf "%s. Actual value was %A but had expected it to be %A." format actual expected @@ -185,39 +186,56 @@ let contains sequence element format = | None -> Tests.failtestf "%s. Sequence did not contain %A." format element -/// Expects the `actual` sequence to contain all elements from `expected` sequence (not taking into account an order of elements). -let containsAll (actual: _ seq) (expected: _ seq) format = - let except seqFirst seqSecond = - seqFirst - |> Seq.filter(fun x -> not(seqSecond |> Seq.exists(fun y -> y = x))) - |> Seq.toList - let shouldContains = except actual expected - let shouldNotContains = except expected actual - let additionalInfo = - if not(shouldNotContains |> Seq.isEmpty) then - sprintf "Should not contains these values but contains: - %A" - shouldNotContains - else "" - let msg = sprintf "Sequence does not contains all elements. - Should contains: - %A - But contains: - %A - Missing values: - %A - %s" - actual expected shouldContains additionalInfo - let isNotCorrect = not (shouldContains |> Seq.isEmpty) || not (shouldNotContains |> Seq.isEmpty) - if isNotCorrect then - Tests.failtestf "%s. - %s" format msg +let inline private formatSet<'a> (ss : 'a seq) : string = + if Seq.isEmpty ss then + "{}" + else + (match box (Seq.nth 0 ss) with + | :? IComparable -> + ss + |> Seq.cast + |> Seq.sort + |> Seq.cast<'a> + |> Seq.map (fun (a : 'a) -> a.ToString()) + | otherwise -> + ss + |> Seq.map (fun a -> a.ToString()) + |> Seq.sort) + |> String.concat ", " + |> sprintf "{%s}" + +/// Expects the `actual` sequence to contain all elements from `expected` +/// sequence (not taking into account an order of elements). Calling this +/// function will enumerate both sequences; they have to be finite. +let containsAll (actual : _ seq) + (expected : _ seq) + format = + let axs, exs = List.ofSeq actual, List.ofSeq expected + let extra, missing = + let ixs = axs |> List.filter (fun a -> exs |> List.exists ((=) a)) + axs |> List.filter (fun a -> not (ixs |> List.exists ((=) a))), + exs |> List.filter (fun e -> not (ixs |> List.exists ((=) e))) + + if List.isEmpty missing then () else + + sprintf "%s. + Sequence `actual` does not contain all `expected` elements. + All elements in `actual`: + %s + All elements in `expected`: + %s + Missing elements from `actual`: + %s + Extra elements in `actual`: + %s" + format (formatSet axs) (formatSet exs) (formatSet missing) (formatSet extra) + |> Tests.failtest /// Expects the `actual` sequence to equal the `expected` one. let sequenceEqual (actual : _ seq) (expected : _ seq) format = use ai = actual.GetEnumerator() use ei = expected.GetEnumerator() - let baseMsg = + let baseMsg = sprintf "%s. Expected value was: %A diff --git a/Expecto/Expecto.fs b/Expecto/Expecto.fs index c36976bf..6ad47ed8 100644 --- a/Expecto/Expecto.fs +++ b/Expecto/Expecto.fs @@ -349,47 +349,74 @@ module Impl = /// Prints a summary given the test result counts summary : TestResultSummary -> unit } + static member silent = + { beforeRun = fun _ -> () + beforeEach = fun _ -> () + passed = fun _ _ -> () + ignored = fun _ _ -> () + failed = fun _ _ _ -> () + exn = fun _ _ _ -> () + summary = fun _ -> () } + static member Default = { beforeRun = fun _tests -> - logger.info ( + logger.logWithAck Info ( eventX "EXPECTO? Running tests...") + |> Async.RunSynchronously beforeEach = fun n -> - - logger.debug ( + logger.logWithAck Debug ( eventX "{testName} starting..." >> setField "testName" n) + |> Async.RunSynchronously + passed = fun n d -> - logger.debug ( + logger.logWithAck Debug ( eventX "{testName} passed in {duration}." >> setField "testName" n >> setField "duration" d) + |> Async.RunSynchronously + ignored = fun n m -> - logger.warn ( + logger.logWithAck Debug ( eventX "{testName} was ignored. {reason}" >> setField "testName" n >> setField "reason" m) + |> Async.RunSynchronously + failed = fun n m d -> - logger.error ( + logger.logWithAck LogLevel.Error ( eventX "{testName} failed in {duration}. {message}" >> setField "testName" n >> setField "duration" d >> setField "message" m) + |> Async.RunSynchronously + exn = fun n e d -> - logger.error ( + logger.logWithAck LogLevel.Error ( eventX "{testName} errored in {duration}" >> setField "testName" n >> setField "duration" d >> addExn e) + |> Async.RunSynchronously + summary = fun summary -> - logger.info ( - eventX "EXPECTO! {total} tests run in {duration} – {passes} passed, {ignores} ignored, {failures} failed, {errors} errored." + let spirit = + if summary.errored.Length + summary.failed.Length = 0 then + "ᕙ໒( ˵ ಠ ╭͜ʖ╮ ಠೃ ˵ )७ᕗ" + else + "( ರ Ĺ̯ ರೃ )" + logger.logWithAck Info ( + eventX "EXPECTO! {total} tests run in {duration} – {passes} passed, {ignores} ignored, {failures} failed, {errors} errored. {spirit}" >> setField "total" summary.total >> setField "duration" summary.duration >> setField "passes" summary.passed.Length >> setField "ignores" summary.ignored.Length >> setField "failures" summary.failed.Length - >> setField "errors" summary.errored.Length)} + >> setField "errors" summary.errored.Length + >> setField "spirit" spirit) + |> Async.RunSynchronously } + static member Summary = { TestPrinters.Default with summary = fun summary -> @@ -460,7 +487,8 @@ module Impl = fun (printers: TestPrinters) (* locks : Locks *) map -> - let beforeOne (name: string, test, wrappedFocusedState) = + let beforeEach (name: string, test, wrappedFocusedState) = + //printfn "--> beforeEach" printers.beforeEach name name, test, wrappedFocusedState @@ -471,6 +499,7 @@ module Impl = result = Ignored ignoredMessage duration = TimeSpan.Zero } | _ -> + //printfn "--> execFocused" next (name, test, wrappedFocusedState) /// Used to constrain parallelism for tests that cannot be run parallel @@ -480,6 +509,7 @@ module Impl = try // here Anthony enterLock () (*locks*) (name, test) + //printfn "--> maybeSequence" next (name, test, wrappedFocusedState) finally exitLock () (*locks*) (name, test) @@ -487,6 +517,7 @@ module Impl = let execOne (name: string, test: TestCode, wrappedFocusedState: WrappedFocusedState) = let w = System.Diagnostics.Stopwatch.StartNew() try + //printfn "--> execOne" test () w.Stop() { name = name @@ -515,6 +546,7 @@ module Impl = duration = w.Elapsed } let printOne (trr : TestRunResult) = + //printfn " --> printOne" match trr.result with | Passed -> printers.passed trr.name trr.duration | Failed message -> printers.failed trr.name message trr.duration @@ -525,7 +557,7 @@ module Impl = let pipeline = execFocused ( maybeSequence ( - beforeOne + beforeEach >> execOne >> printOne ) diff --git a/README.md b/README.md index be1983ea..c500e29e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ compositional (just like Suave and Logary are). * [Sending e\-mail on failure – custom printers](#sending-e-mail-on-failure--custom-printers) * [About test parallelism](#about-test-parallelism) * [About upgrading from Fuchu](#about-upgrading-from-fuchu) + * [Why the strange name?](#why-the-strange-name) + * [What does 'expected to have type TestCode' mean?](#what-does-expected-to-have-type-testcode-mean) ## Installing @@ -471,3 +473,15 @@ and replace with `Expect.equal $4 $3 $1`. ### Why the strange name? ![Expecto expecto](./docs/expecto-patronus-2000x1126.png "This is actually because nuget won't let me publish them with the name 'Expecto', plain and simple.") + +### What does 'expected to have type TestCode' mean? + +If you get an error message like this: + +``` +This expression was expected to have type 'TestCode' but here has type 'unit' +``` + +It means that you have code like `testCase "abc" <| Expect.equal ...`. Instead +you should create a function like so: `testCase "abc" <| fun _ -> Expect.equal +...`.