Fixtures en Go: Extrayendo la infraestructura de las pruebas automatizadas con Axiom
Las pruebas de integración y E2E en Go a menudo mezclan la lógica de negocio con la gestión de infraestructura. El paquete estándar testing no proporciona un mecanismo de fixtures, lo que obliga a los desarrolladores a duplicar código para inicializar clientes, preparar datos y limpiar recursos directamente dentro de las pruebas. Esto reduce la legibilidad, complica el mantenimiento y dificulta la escalabilidad de la suite de pruebas. La solución es implementar fixtures mediante bibliotecas de terceros como Axiom, que permiten describir declarativamente las dependencias con ciclos de vida gestionados.
¿Qué son los fixtures en el contexto de las pruebas
Un fixture no es solo una función auxiliar, sino un recurso con un ciclo de vida claramente definido: creación bajo demanda, limpieza automática después de la ejecución de la prueba, capacidad de depender de otros fixtures e aislamiento de la lógica de aserciones. A diferencia de la gestión manual de conexiones o datos, los fixtures proporcionan:
- Inicialización perezosa: el recurso se crea solo en el primer acceso.
- Limpieza automática: incluso si la prueba falla, se garantiza la limpieza del estado.
- Dependencias declarativas: un fixture puede referenciar a otro sin pasar parámetros manualmente.
- Aislamiento entre pruebas y reintentos: el estado no se filtra entre intentos.
Lenguajes como Python (pytest) o Java (JUnit) tienen estos mecanismos integrados. En Go, deben implementarse explícitamente o usando herramientas especializadas.
El problema sin fixtures: Infraestructura dentro de la prueba
Un test típico en Go sin fixtures se ve así:
func TestGetUser(t *testing.T) {
client, err := NewUserClient()
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
defer client.Close()
user, err := CreateUser(client)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
defer func() {
_ = DeleteUser(client, user.ID)
}()
resp, err := client.GetUser(user.ID)
if err != nil {
t.Fatalf("failed to get user: %v", err)
}
if resp.ID != user.ID {
t.Fatalf("unexpected user id")
}
}
Aquí, todo —desde la creación del cliente hasta la eliminación del usuario— está dentro del cuerpo de la prueba. A medida que crece el número de dependencias (bases de datos, APIs externas, colas de mensajes), este enfoque lleva a:
- Alta duplicación de código.
- Dificultad para realizar cambios (p. ej., actualizar la autenticación del cliente).
- Mayor probabilidad de errores en la lógica de limpieza.
- Pobre legibilidad: la aserción principal se pierde entre detalles de infraestructura.
Implementando fixtures con Axiom
La biblioteca Axiom proporciona un mecanismo para describir fixtures como funciones que devuelven un valor, una función de limpieza y un error. El ciclo de vida se gestiona mediante Runner y Case.
Estructura de ejemplo:
// Fixture client
func UserClientFixture(_ *axiom.Config) (any, func(), error) {
client, err := NewUserClient()
if err != nil {
return nil, nil, err
}
return client, client.Close, nil
}
// Fixture polzovatelya, zavisyaschaya from client
func UserFixture(cfg *axiom.Config) (any, func(), error) {
client := axiom.GetFixture*UserClient
user, err := client.CreateUser()
if err != nil {
return nil, nil, err
}
cleanup := func() { _ = client.DeleteUser(user.ID) }
return user, cleanup, nil
}
// Runner with globalnoy fiksturoy
var runner = axiom.NewRunner(
axiom.WithRunnerFixture("client", UserClientFixture),
)
// Test
func TestGetUser(t *testing.T) {
c := axiom.NewCase(
axiom.WithCaseName("get user by id"),
axiom.WithCaseFixture("user", UserFixture),
)
runner.RunCase(t, c, func(cfg *axiom.Config) {
client := axiom.GetFixture*UserClient
user := axiom.GetFixture*User
resp, err := client.GetUser(user.ID)
if err != nil {
t.Fatalf("failed to get user: %v", err)
}
if resp.ID != user.ID {
t.Fatalf("unexpected user id")
}
})
}
Ventajas clave de este enfoque:
- Separación clara de responsabilidades: los fixtures manejan la infraestructura, las pruebas manejan el comportamiento.
- Reutilización: un fixture de cliente único puede usarse en todas las pruebas mediante Runner.
- Seguridad en reintentos: en un reintento de prueba, todos los fixtures se recrean, previniendo influencia de estados previos.
- Declarativo: las dependencias se declaran explícitamente sin código imperativo en el cuerpo de la prueba.
Cuándo implementar fixtures
Los fixtures son especialmente útiles en estos escenarios:
- Pruebas de integración con múltiples dependencias externas (DB, API, almacenamiento de archivos).
- Pruebas E2E que requieren configuración compleja del entorno.
- Proyectos con un gran número de pruebas (>50–100), donde el mantenimiento es crítico.
- Equipos que buscan uniformidad y predictibilidad en la infraestructura de pruebas.
No uses fixtures para pruebas unitarias simples donde las dependencias se pueden mockear fácilmente mediante interfaces.
Puntos clave
- Los fixtures no están integrados en Go: necesitas implementarlos explícitamente o usar bibliotecas como Axiom.
- El objetivo de los fixtures es extraer la lógica de infraestructura del cuerpo de la prueba.
- Axiom proporciona inicialización perezosa, limpieza automática e aislamiento entre pruebas.
- El enfoque es especialmente efectivo en proyectos grandes con cientos de pruebas de integración.
- Los fixtures no reemplazan a los mocks: los complementan en escenarios que requieren entornos reales.
— Editorial Team
Aún no hay comentarios.