Test.Runner.UnitNode

An xUnit / Python-unittest-style test framework for Gren.

This test runner has:

  1. Suite- and test-level fixtures with setUpSuite/tearDownSuite (once per group) and setUp/tearDown (once per test) — see suite.
  2. Lifecycle failures are first-class: a thrown setUp/tearDown becomes a recorded error, never a silent skip or a crash.
  3. CLI selection by name or glob, verbose per-test timing, and JUnit XML output — all in run.
type alias Suite = Suite

An opaque, fixture-erased test group. Build one with suite.

type Test testFix

A single test, parameterized by the test-fixture type its body receives. Build one with test. Within a suite call, every test must share the same fixture type (it's whatever that suite's setUp produces).

type alias Outcome = Outcome

A test's result: passed, failed an assertion, or errored (threw). Re-exported for reporters/consumers; you rarely match on it directly.

suite :
{ name : String
, setUpSuite : Task String suiteFix
, tearDownSuite : suiteFix -> Task String {}
, setUp : suiteFix -> Task String testFix
, tearDown : testFix -> Task String {}
, tests : Array (Test testFix)
}
-> Suite

Define a test group with the full xUnit lifecycle.

setUpSuite runs once and produces a suite fixture handed to every setUp; setUp runs before each test and produces the test fixture handed to the body and to tearDown. The two fixture type variables are erased here, so the returned Suite is monomorphic and groups with unrelated fixtures share one Array Suite.

test :
String
-> (testFix -> Task String Expectation)
-> Test testFix

Define a single test: a name and a body that, given the suite's test fixture, runs effects and returns an Expectation.

test "is not 1970" <| \_ ->
    Time.now |> Task.map (\t -> Expect.notEqual 0 (Time.posixToMillis t))
noSuiteFixture : Task String {}

A setUpSuite/setUp that produces the empty fixture {} — for groups or tests that need no fixture value.

noFixture : suiteFix -> Task String {}

A setUp or globalSetUp that ignores its fixture and produces {}.

noTearDown : fixture -> Task String {}

A tearDown, tearDownSuite, or globalTearDown that does nothing.

run :
{ name : String
, version : String
, suites : Array Suite
}
-> SimpleProgram a

Build a test executable with no permissions. Use this when your tests are purely computational. For tests that need filesystem, network, or process access, use runWith.

main : Node.SimpleProgram a
main =
    Test.Runner.UnitNode.run
        { name = "my-tests"
        , version = "1.0.0"
        , suites = [ arithmetic, stringTests ]
        }
runWith :
{ name : String
, version : String
, init : (perms -> Task ( Cmd a))
-> Task (Cmd a)
, globalSetUp : perms -> Task String {}
, globalTearDown : perms -> Task String {}
, suites : perms -> Array Suite
}
-> SimpleProgram a

Like run, but you supply the initialization step. Use this when your tests need permissions other than fs + childProcess, or none at all. See Test.Runner.UnitNode.Program.runWith for full documentation.

Writing a suite

import Test.Runner.UnitNode as U
import Expect

arithmetic : U.Suite
arithmetic =
    U.suite
        { name = "Arithmetic"
        , setUpSuite = U.noSuiteFixture
        , tearDownSuite = U.noTearDown
        , setUp = U.noFixture
        , tearDown = U.noTearDown
        , tests =
            [ U.test "adds" <| \_ ->
                Task.succeed (Expect.equal 4 (2 + 2))
            ]
        }

A fixture-bearing suite threads a value from setUp into each test body:

U.suite
    { name = "TempDir"
    , setUpSuite = U.noSuiteFixture
    , tearDownSuite = U.noTearDown
    , setUp = \_ -> FileSystem.makeTempDirectory perms.fs "ut" |> mapErr
    , tearDown = \dir -> FileSystem.remove perms.fs { recursive = True } dir |> mapErr
    , tests =
        [ U.test "writes a file" <| \dir ->
            writeAndCheck dir   -- : Task String Expectation
        ]
    }

Every lifecycle task and every test body works in Task String _: the success channel carries the value (or the test's Expectation), and the error channel is a String you render yourself. Keeping errors as String (rather than an arbitrary type) is what lets the runner print them verbatim instead of mangling them through Debug.toString.