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:
- Red — Escribir un test que falla. La falla confirma que el test es válido y que la funcionalidad aún no existe.
- Green — Escribir el mínimo código necesario para que el test pase. No más.
- 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_RetornaErrorTestConciliarSolicitud_CuandoEstatus EnProceso_ActualizaEstatusTestEmitirReporteR21_EmpresaSinProdigiaRFC_RetornaErrorDominioEjemplo TDD en Go
// PASO 1 — RED: el test falla porque IniciarProcesamiento no existe aúnfunc 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ónfunc (s *Solicitud) IniciarProcesamiento() error { s.Estatus = EnProceso return nil}
// PASO 3 — REFACTOR: agregar validación de estado previofunc (s *Solicitud) IniciarProcesamiento() error { if s.Estatus != ListaParaConciliar { return ErrTransicionInvalida(s.Estatus, EnProceso) } s.Estatus = EnProceso return nil}Ejemplo TDD en NestJS/Jest
// REDdescribe("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"); });});
// GREENexport 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
// REDvoid 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: esCaracterí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:
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:
go test ./features/... -vBDD en NestJS con Jest + Cucumber
// (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
// En Flutter, BDD se aplica a nivel de widget e integraciónvoid 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 /─────────────────────\| Capa | Herramienta por stack | Qué prueba |
|---|---|---|
| Unitarios | Go: testify, NestJS: jest, Dart: flutter_test, Python: pytest | Entidades, value objects, casos de uso con mocks |
| Integración / BDD | Go: godog, NestJS: jest-cucumber, Dart: integration_test | Escenarios completos contra infraestructura real o en memoria |
| E2E | Playwright (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.
- Escribir el escenario Gherkin → define el contrato observable
- Implementar el step definitions → guía el diseño del caso de uso
- Implementar el caso de uso con TDD → guía el diseño del dominio
- 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) ygodog(BDD). En NestJS se usajestyjest-cucumber. En Flutter se usaflutter_test. En Python se usapytest. - 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.