Primeros pasos en ATDD con qed
31 Oct 2026 - Evaristo
En nuestros repositorios, las pruebas que escribimos con TDD suelen ser de bajo nivel. De ahora en adelante, también agregamos pruebas de aceptación de alto nivel para cada funcionalidad nueva. Aquí explicamos la práctica que estamos adoptando y cómo la implementamos con qed.
De TDD a ATDD
Con TDD escribimos una prueba unitaria (que truena) desde la perspectiva del programador; luego escribimos el código que la hace pasar. Con ATDD escribimos primero una prueba de aceptación, que observa el sistema por su interfaz pública desde la perspectiva de quien lo usa.
El ciclo tiene dos bucles. El bucle externo es la prueba de aceptación, que truena mientras la funcionalidad no existe. El bucle interno es TDD, que construye las piezas internas con pruebas unitarias. La prueba de aceptación pasa cuando el comportamiento completo existe.
Una especificación es un conjunto de criterios, escrito en un archivo TOML. Un criterio de aceptación es un elemento de la especificación. Un escenario es la descripción de un criterio en lenguaje natural (prosa). Cada criterio apunta a una prueba de aceptación, que es el mismo escenario escrito como código. Escribimos cada escenario como parte de un criterio y después como prueba. El criterio y su prueba definen qué significa terminar.
qed verifica especificaciones escritas en TOML. Una sola prueba de aceptación que truene hace que qed rechace la especificación.
El flujo de trabajo
Primero preparamos qed; después lo usamos en la siguiente funcionalidad.
1. Declarar la interfaz pública
AGENTS.md debe decir cuál es la interfaz pública, por ejemplo, una API o una CLI.
Las pruebas de aceptación observan el sistema solo por esa interfaz.
2. Agregar qed a una imagen de desarrollo
Creamos Dockerfile.dev a partir de la imagen de producción.
FROM islasgeci/<repositorio>:latest
RUN apt update && apt install --yes curl git
RUN curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh --silent --show-error --fail --output /tmp/elan-init.sh && sh /tmp/elan-init.sh -y
RUN . "$HOME/.elan/env" && git clone https://github.com/tskovlund/qed.git /opt/qed
RUN . "$HOME/.elan/env" && cd /opt/qed && lake build
RUN ln --symbolic --force /opt/qed/.lake/build/bin/qed /usr/local/bin/qed
3. Declarar el servicio de desarrollo
En docker-compose.yml agregamos un servicio con el perfil develop.
services:
develop:
profiles: ["develop"]
build:
context: .
dockerfile: Dockerfile.dev
image: islasgeci/<repositorio>:develop
command: bash
volumes:
- $PWD:/workdir
El perfil mantiene la imagen de desarrollo fuera de la construcción normal.
4. Agregar el objetivo verify a Makefile
En Makefile, el objetivo verify solo llama a qed.
verify:
qed verify specs
El objetivo supone que la imagen de producción ya existe; constrúyela con docker compose build antes de ejecutarlo.
5. Integrar el canal
- name: Verify acceptance criteria
run: docker compose --profile develop run --rm develop make verify
El primer ciclo
Elegimos la siguiente funcionalidad, no una que ya funciona.
1. Escribir la especificación
Escribimos el escenario (la prosa) antes de escribir el código correspondiente de la prueba de aceptación. Esta es una práctica nueva, porque, hasta ahora, escribíamos las pruebas sin escribir el escenario al que corresponden.
Cada funcionalidad puede tener múltiples criterios de aceptación. Podemos escribir los escenarios de todos los criterios antes de escribir la primera prueba de aceptación.
Aquí hay una plantilla para un criterio en specs/<funcionalidad>.spec.toml para Python con pytest.
#:schema https://raw.githubusercontent.com/tskovlund/qed/main/docs/spec.schema.json
name = "<funcionalidad>"
[[criteria]]
description = """
<Escenario de aceptación en lenguaje natural, con ejemplos concretos.>
"""
skip = "Not implemented"
[criteria.verify]
type = "command"
run = "pytest tests/test_acceptance_<funcionalidad>.py::test_<nombre del criterio>"
La prueba en R usa testthat.
[criteria.verify]
type = "command"
run = "Rscript -e \"testthat::test_file('tests/testthat/test_acceptance_<funcionalidad>.R', stop_on_failure = TRUE)\""
Sin stop_on_failure = TRUE, testthat::test_file() termina con código 0 aunque truene la prueba, y qed marca el criterio como aprobado.
Lo verificamos con testthat 3.1.5.
La expresión va con -e: Rscript no tiene la opción --expr.
2. Escribir la prueba que truena
En Python, usamos un archivo por funcionalidad y un caso por criterio.
Si la interfaz es una API de FastAPI, usamos TestClient.
from fastapi.testclient import TestClient
from <modulo>.api import app
client = TestClient(app)
def test_<criterio>():
response = client.post("/<endpoint>")
# <Traducimos el escenario en aserciones sobre el resultado observable.>
assert response.status_code == 200
Si la interfaz es una CLI de Typer, usamos CliRunner.
from typer.testing import CliRunner
from <modulo>.cli import cli
runner = CliRunner()
def test_version():
result = runner.invoke(cli, ["version"])
assert result.exit_code == 0
assert "0.1.0" in result.stdout
Los criterios de Python apuntan al nodo exacto de la prueba.
La prueba en R es un archivo por funcionalidad en tests/testthat/test_acceptance_<funcionalidad>.R; el criterio apunta al archivo completo.
El criterio corre el archivo en una sesión nueva, sin el paquete adjunto.
Por eso el archivo empieza con library(<paquete>): así solo alcanza la interfaz pública y no los ayudantes internos.
library(<paquete>)
describe("Múltiplos de tres y cinco", {
it("dice Fizz para múltiplos de tres", {
expect_equal(fizzbuzz(3), "Fizz")
})
it("dice Buzz para múltiplos de cinco", {
expect_equal(fizzbuzz(5), "Buzz")
})
it("dice FizzBuzz para múltiplos de quince", {
expect_equal(fizzbuzz(15), "FizzBuzz")
})
})
describe("Números ordinarios", {
it("devuelve el número como texto", {
expect_equal(fizzbuzz(7), "7")
})
})
Cuando la prueba existe y truena por la razón correcta, quitamos el skip de specs/<funcionalidad>.spec.toml para que qed rechace la especificación y make verify truene.
3. Rojo
Ejecutamos make verify dentro de la imagen de desarrollo.
La prueba truena porque el comportamiento todavía no existe.
4. Implementar hasta el verde
Implementamos con TDD las piezas internas. Cada iteración del bucle interno de TDD nos acerca un poco más a pasar la prueba de aceptación del bucle externo de ATDD. Para pasar de rojo a verde en una iteración de ATDD, nos tomará muchas iteraciones de TDD.
Conclusión
La especificación queda en el repositorio y el canal la verifica en cada push. El siguiente proyecto empieza con un criterio: escribimos su escenario y su prueba. Los detalles de qed están en su documentación.