Skip to content

TDD y BDD

Test-Driven Development (TDD) y Behavior-Driven Development (BDD) son prácticas de desarrollo que invierten el orden habitual de escribir código: las pruebas se escriben antes que la implementación. No son metodologías de testing; son metodologías de diseño que usan los tests como herramienta para guiar la arquitectura del código.

En GM Transport, TDD y BDD son el mecanismo que verifica que la lógica de dominio es correcta antes de que llegue a producción. Su adopción en GM Fiscal Backend establece el patrón que se replica en el resto del stack.

Test-Driven Development (TDD)

TDD opera en tres pasos que forman un ciclo corto:

  1. Red — Escribir un test que falla. La falla confirma que el test es válido y que la funcionalidad aún no existe.
  2. Green — Escribir el mínimo código necesario para que el test pase. No más.
  3. Refactor — Mejorar el código sin cambiar su comportamiento. Los tests son la red de seguridad.

El ciclo dura minutos, no horas. Cada iteración produce un pequeño incremento de funcionalidad verificado.

graph LR
    R["🔴 Red\nEscribir test\nque falla"] --> G["🟢 Green\nMínimo código\npara pasar"] --> RF["🔵 Refactor\nMejorar sin\ncambiar comportamiento"] --> R

Convención de nombres

En Go, el patrón de nombres de test en GM Transport es:

Test<NombreUC>_<Condición>_<ResultadoEsperado>

Ejemplos:

TestConciliarSolicitud_CuandoEstatusConciliada_RetornaError
TestConciliarSolicitud_CuandoEstatus EnProceso_ActualizaEstatus
TestEmitirReporteR21_EmpresaSinProdigiaRFC_RetornaErrorDominio

Ejemplo TDD en Go

// PASO 1 — RED: el test falla porque IniciarProcesamiento no existe aún
func TestSolicitud_IniciarProcesamiento_CuandoEstatusListaParaConciliar_CambiaAEnProceso(t *testing.T) {
s := solicitud.Solicitud{
UUID: "sol-001",
Estatus: solicitud.ListaParaConciliar,
}
err := s.IniciarProcesamiento()
require.NoError(t, err)
assert.Equal(t, solicitud.EnProceso, s.Estatus)
}
// PASO 2 — GREEN: mínima implementación
func (s *Solicitud) IniciarProcesamiento() error {
s.Estatus = EnProceso
return nil
}
// PASO 3 — REFACTOR: agregar validación de estado previo
func (s *Solicitud) IniciarProcesamiento() error {
if s.Estatus != ListaParaConciliar {
return ErrTransicionInvalida(s.Estatus, EnProceso)
}
s.Estatus = EnProceso
return nil
}

Ejemplo TDD en NestJS/Jest

// RED
describe("RFC ValueObject", () => {
it("debe lanzar InvalidRFCError si el formato es incorrecto", () => {
expect(() => new RFC("no-es-rfc")).toThrow(InvalidRFCError);
});
it("debe normalizar el RFC a mayúsculas", () => {
const rfc = new RFC("xaxx010101000");
expect(rfc.toString()).toBe("XAXX010101000");
});
});
// GREEN
export class RFC {
private readonly value: string;
constructor(value: string) {
const normalized = value.toUpperCase();
if (!/^[A-Z&Ñ]{3,4}[0-9]{6}[A-Z0-9]{3}$/.test(normalized)) {
throw new InvalidRFCError(value);
}
this.value = normalized;
}
toString(): string {
return this.value;
}
}

Ejemplo TDD en Flutter/Dart

// RED
void main() {
group('Solicitud', () {
test('debe cambiar estatus a EnProceso cuando está ListaParaConciliar', () {
final solicitud = Solicitud(
uuid: 'sol-001',
estatus: EstatusSolicitud.listaParaConciliar,
);
final result = solicitud.iniciarProcesamiento();
expect(result.isRight(), true);
expect(result.getOrElse(() => solicitud).estatus,
EstatusSolicitud.enProceso);
});
test('debe retornar Failure si el estatus no es ListaParaConciliar', () {
final solicitud = Solicitud(
uuid: 'sol-001',
estatus: EstatusSolicitud.conciliada,
);
final result = solicitud.iniciarProcesamiento();
expect(result.isLeft(), true);
});
});
}

Behavior-Driven Development (BDD)

BDD extiende TDD elevando el nivel de abstracción: en lugar de tests unitarios de bajo nivel, se definen escenarios de comportamiento en lenguaje natural usando la sintaxis Gherkin.

Gherkin es legible por personas no técnicas. Los escenarios BDD son la especificación ejecutable del comportamiento del sistema, verificable automáticamente.

Sintaxis Gherkin

# language: es
Característica: Conciliación de solicitudes fiscales
Como sistema de conciliación
Quiero procesar solicitudes de conciliación
Para que las empresas puedan verificar su cumplimiento fiscal
Escenario: Conciliación exitosa con facturas coincidentes
Dado que existe una solicitud "sol-001" con estatus "ListaParaConciliar"
Y que la empresa tiene 10 facturas en el ERP para el período
Y que el SAT reporta 10 facturas para el mismo período
Cuando se ejecuta la conciliación
Entonces el estatus de la solicitud debe ser "Conciliada"
Y el reporte debe contener 10 facturas con estatus "Coincidente"
Escenario: Conciliación detecta facturas faltantes en ERP
Dado que existe una solicitud "sol-002" con estatus "ListaParaConciliar"
Y que la empresa tiene 8 facturas en el ERP
Y que el SAT reporta 10 facturas para el mismo período
Cuando se ejecuta la conciliación
Entonces el reporte debe contener 2 facturas con estatus "SoloEnSAT"
Y el estatus de la solicitud debe ser "Conciliada"

BDD en Go con godog

GM Fiscal usa cucumber/godog para ejecutar escenarios Gherkin como tests de integración:

features/conciliacion.go
func InitializeScenario(ctx *godog.ScenarioContext) {
c := &conciliacionContext{}
ctx.Step(`^que existe una solicitud "([^"]*)" con estatus "([^"]*)"$`,
c.existeSolicitudConEstatus)
ctx.Step(`^que la empresa tiene (\d+) facturas en el ERP para el período$`,
c.empresaTieneFacturasERP)
ctx.Step(`^el SAT reporta (\d+) facturas para el mismo período$`,
c.satReportaFacturas)
ctx.Step(`^se ejecuta la conciliación$`,
c.seEjecutaConciliacion)
ctx.Step(`^el estatus de la solicitud debe ser "([^"]*)"$`,
c.verificaEstatusSolicitud)
ctx.Step(`^el reporte debe contener (\d+) facturas con estatus "([^"]*)"$`,
c.verificaFacturasEnReporte)
}
func (c *conciliacionContext) seEjecutaConciliacion(ctx context.Context) error {
uc := conciliacion.NewConciliarSolicitudUseCase(
c.solicitudRepo,
c.conciliacionRepo,
c.erpGateway,
c.satGateway,
)
result, err := uc.Execute(conciliacion.ConciliarCommand{
UUID: c.solicitudUUID,
})
c.result = result
return err
}

Para ejecutar los tests BDD:

Terminal window
go test ./features/... -v

BDD en NestJS con Jest + Cucumber

features/conciliacion.feature
// (mismo archivo Gherkin, ejecutado con jest-cucumber)
import { defineFeature, loadFeature } from "jest-cucumber";
const feature = loadFeature("./features/conciliacion.feature");
defineFeature(feature, (test) => {
test("Conciliación exitosa con facturas coincidentes", ({
given,
and,
when,
then,
}) => {
let solicitud: Solicitud;
let resultado: ConciliacionResult;
given(
/^que existe una solicitud "([^"]*)" con estatus "([^"]*)"$/,
(uuid, estatus) => {
solicitud = Solicitud.reconstituir({ uuid, estatus });
},
);
when("se ejecuta la conciliación", async () => {
const useCase = new ConciliarSolicitudUseCase(mockRepos);
resultado = await useCase.execute({ uuid: solicitud.uuid });
});
then(/^el estatus de la solicitud debe ser "([^"]*)"$/, (esperado) => {
expect(resultado.solicitud.estatus).toBe(esperado);
});
});
});

BDD en Flutter/Dart con BDD Widget Tests

lib/features/auth/test/login_bdd_test.dart
// En Flutter, BDD se aplica a nivel de widget e integración
void main() {
group('Funcionalidad: Login de usuario', () {
testWidgets(
'Escenario: Login exitoso con credenciales válidas',
(tester) async {
// Dado que el usuario está en la pantalla de login
await tester.pumpWidget(const ProviderScope(child: LoginScreen()));
// Cuando introduce su RFC y contraseña válidos
await tester.enterText(find.byKey(const Key('rfc_field')), 'XAXX010101000');
await tester.enterText(find.byKey(const Key('password_field')), 'contraseña-segura');
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle();
// Entonces debe ver la pantalla principal
expect(find.byType(HomeScreen), findsOneWidget);
expect(find.byType(LoginScreen), findsNothing);
},
);
});
}

La pirámide de tests en GM Transport

La proporción recomendada de tests sigue la pirámide estándar, adaptada al stack:

/ \
/ E2E \ ← Pocos, lentos, confirman flujos completos
/─────────────\
/ Integración \ ← BDD / godog / scenarios
/─────────────────\
/ Unitarios (TDD) \ ← Muchos, rápidos, prueban dominio aislado
/─────────────────────\
CapaHerramienta por stackQué prueba
UnitariosGo: testify, NestJS: jest, Dart: flutter_test, Python: pytestEntidades, value objects, casos de uso con mocks
Integración / BDDGo: godog, NestJS: jest-cucumber, Dart: integration_testEscenarios completos contra infraestructura real o en memoria
E2EPlaywright (web), Maestro (móvil)Flujos completos del usuario

Cuándo escribir cada tipo de test

La regla de GM Transport es Outside-In: se empieza definiendo el comportamiento esperado a nivel BDD y se baja hacia los tests unitarios conforme se implementan los componentes internos.

  1. Escribir el escenario Gherkin → define el contrato observable
  2. Implementar el step definitions → guía el diseño del caso de uso
  3. Implementar el caso de uso con TDD → guía el diseño del dominio
  4. Implementar las entidades y value objects con TDD → mínima lógica verificada

Este flujo garantiza que cada línea de código existe porque un comportamiento de negocio lo requiere.

Resumen

  • TDD guía el diseño del código mediante el ciclo Red-Green-Refactor. El test es la especificación, no el código.
  • BDD eleva el nivel de abstracción con escenarios Gherkin legibles por negocio y técnicos.
  • En Go se usa testify (TDD) y godog (BDD). En NestJS se usa jest y jest-cucumber. En Flutter se usa flutter_test. En Python se usa pytest.
  • La convención de nombres es Test<UC>_<Condición>_<Resultado> para tests unitarios.
  • El flujo Outside-In (BDD → TDD → unitarios) garantiza que cada componente existe porque un escenario de negocio lo requiere.
  • Los tests son la red de seguridad que hace posible el refactoring continuo sin regresiones.