DocsStandard library

Testing.

Write expectations, group tests, and choose handlers and reports for a test run.

A test is a small function that checks something you expect to be true. Fango bundles Expect for those checks and Test for organizing and running them.

API reference: Expect · Test · Test.Reporter

Run your first tests.

Save this as Tests.fango:

import Expect
import String
import Test exposing (Test, describe, test)

suite : Test {}
suite =
    describe "String.split"
        [ test "splits on the separator" {
            Expect.equal ["a", "b"] (String.split "," "a,b")
          }
        , test "keeps text without a separator" {
            Expect.equal ["abc"] (String.split "," "abc")
          }
        ]

main() = Test.run suite

Run it like any other program:

fango run Tests.fango
TEST RUN PASSED

Passed:  2
Failed:  0
Skipped: 0
Todo:    0

describe names a group, and test names one body. Test {} means the bodies need no effects besides their expectations. Groups can contain other groups, so a larger suite can follow the shape of your program. There isn’t a separate fango test command.

State what you expect.

Expect.equal expected actual puts the expected value first. You can also pipe the actual value into it: String.split "," "a,b" |> Expect.equal ["a", "b"].

Expectations are statements, not values you collect. A failing one aborts its body through the Expect effect, and the runner records the failure and moves on to the next test. That’s why a body can be a plain sequence of checks. A report gives the group’s path, the expectation’s file and line, and the failure message. If a helper makes the expectation, the location points inside that helper.

notEqual checks inequality, and lessThan, greaterThan, atMost, and atLeast compare against the first argument. Failure messages show values that have Show and use <value> for the rest, so you don’t have to derive Show just to test a type.

Expect.ok, Expect.err, and Expect.just check a constructor and return the value inside it. That makes a sequence of checks easy to read:

import Expect
import Fail
import Test
import Time.Date as Date

main() = Test.run (Test.test "month-end renewal" {
    issued = Expect.ok (Fail.attempt { Date.make 2026 1 31 })
    renewal = Expect.ok (Fail.attempt { Date.addMonths 1 issued })
    Expect.equal "2026-02-28" (Date.toIso renewal)
})
TEST RUN PASSED

Passed:  1
Failed:  0
Skipped: 0
Todo:    0

To assert that a calculation fails, handle its own failure first, then check the result with Expect.err. Expect.onFail "context" action adds context to a failing expectation while keeping its original location.

Testing effectful code.

The code under test can use effects. Choose a handler inside the body when each test should start afresh. Here the clock is fixed, so the result is independent of when the test runs:

import Expect
import Fail
import Test
import Time.Clock as Clock exposing (Clock)
import Time.DateTime as DateTime

greeting : () ->{Clock} String
greeting() =
    if DateTime.hour Clock.now() < 12 then "Good morning" else "Good afternoon"

main() = Test.run (Test.test "morning greeting" {
    morning = Expect.ok (Fail.attempt { DateTime.parse "2026-10-07T09:30:00-03:00" })
    Expect.equal "Good morning" (Clock.runFixed morning greeting)
})
TEST RUN PASSED

Passed:  1
Failed:  0
Skipped: 0
Todo:    0

Use stepped clocks when readings need to advance, seeded randomness for repeatable draws, HTTP mocking to exercise client and server code without sockets, and Console.runTest or File.runMemory for code that reads and writes.

Effects left in a body appear in the suite’s row, so a suite whose tests read real files has File.FileSystem in it. Other effects pass through execution, so you can also install a handler around the whole run when tests should share it. Expectation failures stay separate from the application’s Fail effects. Unhandled effects and runtime crashes propagate; the framework doesn’t turn them into expectation failures.

Focus and unfinished tests.

Wrap a test or group in Test.only to focus on it, or Test.skip to leave it out. Test.todo "name" marks a test you haven’t written yet. Focus skips other bodies; an explicit skip also skips any todos in its subtree.

These are useful while editing, but they make the run incomplete. Test.run returns a Process.Status, which main returns in turn. Only a complete passing suite gives status 0. Failures and incomplete runs give status 1, even if every body that ran passed. Empty groups, empty names, and duplicate member names are group failures, and don’t stop the group’s tests from running.

Choose a report.

Test.run reads the program’s command-line arguments. With no options, it prints failures and a summary, as above. To see progress or each test’s name, pass a format after the source path:

fango run Tests.fango --format progress
fango run Tests.fango --format documentation
fango run Tests.fango --format silent --json report.json

progress prints a dot for each passing test, documentation prints groups and each case’s outcome, and silent prints nothing. --json PATH also writes a structured report, with any of these formats, that you can feed to other tools. It replaces an existing file, and its directory must already exist. --help prints usage without running the tests. Invalid options or a report that can’t be written exit with status 2.

Use the summary directly.

When you want a result instead of command-line output and an exit policy, use Test.execute with a reporting handler:

import Expect
import Test

main() =
    summary = Test.silent {
        Test.execute (Test.test "addition" { Expect.equal 4 (2 + 2) })
    }
    print summary.passed
    print summary.verdict
1
RunPassed

execute reports structured events through the Reporter effect and returns a Summary. silent discards those events; console, progress, and documentation render them without exiting. A pure suite can execute under silent without any service. For your own report format, handle the events described in Test.Reporter.