Testing en Gleam: gleeunit y cómo escribir tests en un lenguaje tipado sin excepciones

El sistema de tipos de Gleam ya elimina en compilación una buena parte de los bugs que en otros lenguajes solo se detectan con tests. Pero los tipos no lo comprueban todo: la lógica de negocio, los casos límite, el comportamiento de funciones con efectos secundarios, todo eso necesita tests. Gleam tiene gleeunit, su framework de testing oficial, y el comando gleam test para ejecutarlos.

Configurar gleeunit

gleeunit se añade como dependencia de desarrollo. Si creas un proyecto con gleam new, ya viene incluido en gleam.toml. Si no, puedes añadirlo manualmente:

gleam add --dev gleeunit

El fichero gleam.toml:

[dev-dependencies]
gleeunit = ">= 1.0.0"

Los ficheros de test van en el directorio test/ del proyecto y tienen el sufijo _test.gleam:

mi_proyecto/
??? src/
?   ??? mi_proyecto.gleam
??? test/
?   ??? mi_proyecto_test.gleam
??? gleam.toml

Escribir el primer test

// test/mi_proyecto_test.gleam
import gleeunit
import gleeunit/should
import mi_proyecto

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

pub fn suma_test() {
  mi_proyecto.suma(2, 3)
  |> should.equal(5)
}

pub fn suma_negativa_test() {
  mi_proyecto.suma(-1, -2)
  |> should.equal(-3)
}

Las funciones de test tienen el sufijo _test. gleeunit las detecta automáticamente y las ejecuta. El resultado va al final de la función usando el pipe operator con should.equal. Ese estilo de «valor |> should.equal(esperado)» es muy natural en Gleam.

Las aserciones de gleeunit/should

import gleeunit/should

// Igualdad
value |> should.equal(expected)
value |> should.not_equal(other)

// Booleanos
value |> should.be_true
value |> should.be_false

// Option
option_value |> should.be_some
option_value |> should.be_none
option_value |> should.be_some |> should.equal(42)

// Result
result_value |> should.be_ok
result_value |> should.be_error
result_value |> should.be_ok |> should.equal(42)

Testar código con Result y Option

Como Gleam no tiene excepciones, los tests con Result y Option son muy directos. No hay que simular excepciones ni usar fixtures especiales:

import gleeunit/should
import gleam/int

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

pub fn parse_numero_invalido_test() {
  int.parse("abc")
  |> should.be_error
}

pub fn divide_por_cero_test() {
  mi_modulo.divide(10, 0)
  |> should.be_error
  |> should.equal("División por cero")
}

Testar actores y código concurrente

Para testar actores del módulo gleam_otp, el patrón habitual es arrancar el actor, enviarle mensajes y comprobar el estado o las respuestas con un Subject tipado:

import gleeunit/should
import gleam/otp/actor
import gleam/erlang/process
import mi_proyecto/contador

pub fn contador_incrementa_test() {
  let assert Ok(actor) = contador.start(0)

  process.send(actor, contador.Increment)
  process.send(actor, contador.Increment)

  let subject = process.new_subject()
  process.send(actor, contador.GetCount(subject))

  let assert Ok(count) = process.receive(subject, 1000)
  count |> should.equal(2)

  actor.stop(actor)
}

Ejecutar los tests

# Ejecutar todos los tests
gleam test

# Ejecutar tests para un target específico
gleam test --target javascript
gleam test --target erlang

La salida indica qué tests han pasado, cuáles han fallado y el motivo del fallo con el valor real y el esperado. No hace falta configuración adicional: gleam test descubre y ejecuta todo lo que haya en el directorio test/.

Por qué los tests son más simples con tipos

El sistema de tipos de Gleam tiene un efecto directo en la escritura de tests. Como no hay null, no necesitas tests para «¿qué pasa si el valor es null?». Como no hay excepciones, no necesitas tests para «¿qué pasa si lanza una excepción inesperada?». Los tests se concentran en la lógica real: ¿devuelve el valor correcto para esta entrada? ¿maneja bien los casos de error representados como Result?

Eso no significa que necesites menos tests, pero sí que los tests que escribes son más directos. La cobertura de tipos la da el compilador; la cobertura de lógica la dan los tests.

Integración con CI

Como gleam test es un comando único sin configuración extra, integrarlo en cualquier sistema de CI es trivial. Un pipeline básico en GitHub Actions para un proyecto Gleam:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: erlef/setup-beam@v1
        with:
          otp-version: "26"
          gleam-version: "1.7.0"
      - run: gleam test

Con eso tienes tests automáticos en cada push. El compilador de Gleam también puede fallar el CI si hay errores de tipos, lo que da una doble red de seguridad: primero el compilador, luego los tests.

Con este artículo cerramos la serie sobre Gleam. Hemos cubierto desde la introducción al lenguaje hasta el testing, pasando por tipos, concurrencia, interop con Elixir y Erlang, el target JavaScript y el ecosistema de paquetes. Gleam 1.x es un lenguaje maduro y con ideas claras; si el tipado estático en la BEAM te llama la atención, merece la pena dedicarle tiempo.

Imagen: Pexels / Daniil Komov

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP