Gleam Testing

Gleam tests are regular Gleam functions with names ending in _test. The gleeunit test runner discovers and executes them automatically. Good tests verify behavior, catch regressions, and document how your code works through examples.

Test Project Setup

// gleam.toml — gleeunit is a dev dependency
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

// test/my_app_test.gleam
import gleeunit
import gleeunit/should

pub fn main() {
  gleeunit.main()   // required: runs all tests
}

Writing Your First Test

import gleeunit/should
import my_app/math

pub fn add_test() {
  math.add(2, 3)
  |> should.equal(5)
}

pub fn subtract_test() {
  math.subtract(10, 4)
  |> should.equal(6)
}

Run tests with:

gleam test

Test Output
──────────────────────────────────────────────────
   Compiling my_app
    Finished in 0.35s

..
2 tests, 0 failures   ← each dot = one passing test

The should Module


should Assertion Functions
──────────────────────────────────────────────────
should.equal(expected)        → value == expected
should.not_equal(unexpected)  → value != unexpected
should.be_true()              → value == True
should.be_false()             → value == False
should.be_ok()                → value is Ok(_)
should.be_error()             → value is Error(_)
should.be_some()              → value is Some(_)
should.be_none()              → value is None
import gleeunit/should
import gleam/int

pub fn parse_valid_test() {
  int.parse("42")
  |> should.be_ok()
  |> should.equal(42)
}

pub fn parse_invalid_test() {
  int.parse("not a number")
  |> should.be_error()
}

Testing Custom Types

import my_app/order.{type Order, Order, Fulfilled, Cancelled}
import gleeunit/should

pub fn fulfil_order_test() {
  let order = Order(id: 1, status: Pending, total: 500.0)
  let fulfilled = order.fulfil(order)
  fulfilled.status |> should.equal(Fulfilled)
}

pub fn cancel_fulfilled_order_test() {
  let order = Order(id: 1, status: Fulfilled, total: 500.0)
  let result = order.cancel(order)
  result |> should.be_error()
}

Test File Organization


Mirror src/ structure in test/:
──────────────────────────────────────────────────
src/
  math.gleam
  user/
    auth.gleam
    profile.gleam

test/
  math_test.gleam          ← tests for math
  user/
    auth_test.gleam        ← tests for auth
    profile_test.gleam     ← tests for profile

Testing with Setup Data

import gleeunit/should
import my_app/user

fn make_user() {
  user.User(
    id: 1,
    name: "Test User",
    email: "test@example.com",
    role: user.Admin
  )
}

pub fn user_is_admin_test() {
  make_user()
  |> user.is_admin
  |> should.be_true()
}

pub fn user_display_name_test() {
  make_user()
  |> user.display_name
  |> should.equal("Test User")
}

Testing Pipelines

Pipelines in tests read like documentation:

import my_app/text_processor
import gleeunit/should

pub fn normalize_test() {
  "  HELLO WORLD  "
  |> text_processor.normalize
  |> should.equal("hello world")
}

pub fn word_count_test() {
  "the quick brown fox"
  |> text_processor.word_count
  |> should.equal(4)
}

Testing Error Paths

import my_app/validator
import gleeunit/should

pub fn empty_name_test() {
  validator.validate_name("")
  |> should.be_error()
}

pub fn short_password_test() {
  validator.validate_password("abc")
  |> should.be_error()
}

pub fn valid_password_test() {
  validator.validate_password("SecurePass123!")
  |> should.be_ok()
}

Running Specific Tests

gleam test                      // run all tests
gleam test --module math_test   // run one test module

Test Naming Conventions


Good Test Names — describe the scenario:
──────────────────────────────────────────────────
pub fn add_two_positive_numbers_test()
pub fn returns_error_for_empty_input_test()
pub fn discount_applied_when_user_is_premium_test()
pub fn list_first_on_empty_list_test()

Bad Test Names — too vague:
──────────────────────────────────────────────────
pub fn test1_test()
pub fn math_test()       ← conflicts with module name
pub fn works_test()

Property-Based Testing

The qcheck package adds property-based testing — generating random inputs to find edge cases automatically:

// gleam add qcheck --dev

import qcheck

pub fn reverse_twice_is_identity_test() {
  use list <- qcheck.run(qcheck.list_of(qcheck.int()))
  let double_reversed = list |> list.reverse |> list.reverse
  double_reversed == list
}

Practical Example — Full Module Test

// test/cart_test.gleam
import gleeunit
import gleeunit/should
import my_app/cart.{Cart, Item}

pub fn main() { gleeunit.main() }

fn empty_cart() { Cart(items: [], discount: 0.0) }

pub fn empty_cart_total_test() {
  empty_cart()
  |> cart.total
  |> should.equal(0.0)
}

pub fn add_item_increases_total_test() {
  empty_cart()
  |> cart.add_item(Item(name: "Pen", price: 10.0, qty: 3))
  |> cart.total
  |> should.equal(30.0)
}

pub fn discount_applied_test() {
  empty_cart()
  |> cart.add_item(Item(name: "Book", price: 200.0, qty: 1))
  |> cart.apply_discount(0.10)
  |> cart.total
  |> should.equal(180.0)
}

Key Points


Testing Essentials
──────────────────────────────────────────────────
1. Test files live in test/, named *_test.gleam
2. Test functions end in _test
3. Every test file needs: pub fn main() { gleeunit.main() }
4. Use |> should.equal(expected) for assertions
5. Use should.be_ok() / should.be_error() for Results
6. Use helper functions to build shared test data
7. gleam test runs all tests; --module targets one file
8. Name tests to describe the exact scenario tested

Testing in Gleam is fast, readable, and low-friction. Because the type system catches many bugs at compile time, tests focus on behavior and business logic rather than defending against type errors. Well-named tests become the most accurate documentation your codebase has.

Leave a Comment

Your email address will not be published. Required fields are marked *