A Gleam test runner with random ordering, tagging, and CLI filtering. It is a drop-in replacement for gleeunit if you're already using asserts.
-
Swap gleeunit for unitest:
gleam remove gleeunit gleam add unitest --dev gleam clean
-
Open
test/project_test.gleamand replaceimport gleeunitwithimport unitestandgleeunit.main()withunitest.main().
Create test/yourapp_test.gleam:
import unitest
pub fn main() {
unitest.main()
}
pub fn addition_test() {
assert 1 + 1 == 2
}Run with gleam test.
- Random test ordering with reproducible seeds
- Test tagging for categorization and filtering
- Runtime guards for conditional test execution (platform, version checks)
- CLI filtering by file path, line number, test name, or tag
- Parallel test execution with configurable worker count
- Streaming dot (default) or table output format
- It pulls in a lot of extra dependencies to offer the features above.
- Wildcard filtering
- Smarter tag discovery
gleam test # Random order
gleam test -- --seed 123 # Reproducible order
gleam test -- --reporter table # Table format output
gleam test -- --reporter table --sort time # Table sorted by duration (slowest first)
gleam test -- --reporter table --sort name # Table sorted alphabetically
gleam test -- --reporter table --sort time --sort-rev # Table sorted fastest first
gleam test -- test/my_mod_test.gleam # All tests in file
gleam test -- test/my_mod_test.gleam:42 # Test at line 42
gleam test -- --test my_mod_test.add_test # Single test by name
gleam test -- --tag slow # Tests with tag
gleam test -- test/foo_test.gleam --tag slow # Combine file + tag
gleam test -- --workers 4 # Parallel with 4 workers
gleam test -- --timeout 30000 # Per-test timeout in milliseconds (0 disables)The test/ prefix can be omitted from file paths: my_mod_test.gleam will match test/my_mod_test.gleam. Absolute paths and Windows-style separators (C:\proj\test\my_mod_test.gleam:42) also work.
Note
Timeouts on the JavaScript target. A test that synchronously hangs (an
infinite loop with no await) can only be interrupted in the parallel mode,
which runs tests in worker threads. The sequential and async modes run on
the main thread, where a synchronous hang blocks the event loop and the
timeout cannot fire.
Make the table reporter the default so you don't need --reporter table on every run:
import unitest
pub fn main() {
unitest.defaults()
|> unitest.table_reporter
|> unitest.run
}The --reporter CLI flag still overrides this.
Mark tests for selective execution:
import unitest
pub fn slow_test() {
use <- unitest.tag("slow")
// slow test code
}
pub fn integration_db_test() {
use <- unitest.tags(["integration", "database"])
// integration test code
}Tests can return Result(a, e) instead of Nil. When enabled via unitest.check_results(True), returning Error(reason) is treated as a test failure:
import unitest
pub fn main() {
unitest.defaults()
|> unitest.check_results(True)
|> unitest.run
}
pub fn validation_test() -> Result(Nil, String) {
case validate_config() {
Ok(_) -> Ok(Nil)
Error(msg) -> Error(msg)
}
}This is useful for tests that naturally work with Result types, avoiding the need for let assert Ok(_) = ... patterns.
Skip tests at runtime based on conditions that can't be checked at compile time:
import unitest
pub fn otp27_feature_test() {
use <- unitest.guard(otp_version() >= 27)
// test runs only if OTP >= 27
}
pub fn requires_database_test() {
use <- unitest.guard(envoy.get("DATABASE_URL") |> result.is_ok)
// test runs only if DATABASE_URL is set
}Guards and tags can be combined:
pub fn slow_integration_test() {
use <- unitest.tag("slow")
use <- unitest.guard(envoy.get("CI") |> result.is_ok)
// slow test that only runs in CI
}Skipped tests show as S in the output.
Skip certain tags unless explicitly requested:
import unitest
pub fn main() {
unitest.defaults()
|> unitest.ignored_tags(["slow"])
|> unitest.run
}Tests tagged "slow" will show as S (skipped).
Override with gleam test -- --tag slow.