Cuando los suites de tests crecen, la diferencia entre un test unitario de milisegundos y un test de integración de diez segundos se vuelve un problema de productividad. Si cada vez que ejecutas go test ./... tienes que esperar a que se levante un contenedor de Docker o se haga una llamada a una API externa, el ciclo de feedback se rompe. Para evitar esto, Go ofrece dos mecanismos para filtrar la ejecución de pruebas: los Build Tags (a nivel de compilación) y el Short Mode (a nivel de ejecución).
Los Build Tags [disponible desde Go 1.17] actúan como directivas para el compilador. Si un archivo de test tiene la etiqueta //go:build integration, ese archivo ni siquiera se compilará ni se incluirá en el paquete de pruebas a menos que pases explícitamente el flag -tags integration. Esto es ideal cuando el código de test importa paquetes que no quieres que se carguen en un entorno normal (por ejemplo, drivers de bases de datos pesados o SDKs de la nube). Por el contrario, testing.Short() es una función del paquete testing que consulta un flag de ejecución. Si ejecutas go test -short, la función devuelve true. Esto no evita la compilación del archivo, pero permite que el test se salte durante la ejecución mediante t.Skip().
Debes usar Build Tags cuando quieras aislar archivos enteros que contienen dependencias externas pesadas o lógica que no debe existir en el binario de pruebas estándar. Usa Short Mode cuando tengas tests que, siendo parte de la suite normal, tengan un costo de tiempo elevado pero que no requieran una infraestructura externa compleja. Si te equivocas con los Build Tags y etiquetas un archivo que contiene lógica necesaria para que tu aplicación funcione en producción, el compilador fallará al intentar construir el binario sin la etiqueta correspondiente. Si usas testing.Short() de forma errática en tests críticos que no son lentos, corres el riesgo de que el pipeline de CI pase con éxito aunque la lógica de negocio esté rota, simplemente porque el desarrollador olvidó que esos tests eran “opcionales” en modo corto.
// Para probar este ejemplo, se requiere tener al menos dos archivos
// o simular la estructura de archivos de un paquete.
// --- api.go ---
package main
import (
"errors"
"fmt"
)
type Client struct {
BaseURL string
}
func (c *Client) FetchData(id string) (string, error) {
if id == "" {
return "", errors.New("id vacío")
}
return fmt.Sprintf("data_for_%s", id), nil
}
// --- api_test.go ---
// package main_test (en un entorno real usarías package main_test para evitar exportación)
package main
import (
"testing"
"time"
)
// TestFetchData es un test unitario puro y rápido.
// Se ejecuta siempre con 'go test ./...'
func TestFetchData(t *testing.T) {
c := &Client{BaseURL: "http://localhost"}
got, err := c.FetchData("123")
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if got != "data_for_123" {
t.Errorf("esperado data_for_123, obtenido %s", got)
}
}
// TestHeavyOperation usa testing.Short() para evitar esperas innecesarias.
// Se salta si ejecutas 'go test -short'
func TestHeavyOperation(t *testing.T) {
if testing.Short() {
t.Skip("saltando test pesado en modo corto")
}
// Simulamos una operación que tarda tiempo
time.Sleep(100 * time.Millisecond)
t.Log("operación pesada completada")
}
// --- api_integration_test.go ---
// IMPORTANTE: Este archivo requiere la directiva build tag.
// Para compilarlo: go test -v -tags=integration .
//go:build integration
package main
import (
"testing"
)
// TestIntegration_ExternalAPI solo se compila si usas -tags integration.
// Es ideal para tests que necesitan una base de datos real o red.
func TestIntegration_ExternalAPI(t *testing.T) {
// En un entorno real, aquí levantaríamos un contenedor o usaríamos un mock real
// que requiera conectividad de red.
client := &Client{BaseURL: "https://api.servicio-real.com"}
_, err := client.FetchData("live_id")
if err != nil {
// En un test de integración real, esto podría fallar si no hay internet
t.Logf("Nota: El test de integración falló o requiere entorno real: %v", err)
}
}
Desglose del ejemplo
TestFetchData: Es un test unitario estándar. No tiene condiciones ni etiquetas. Se ejecuta siempre, lo que garantiza que la lógica básica deClientsea correcta en cada commit.TestHeavyOperation: Aquí aplicamostesting.Short(). Si ejecutasgo test -short, el test se marca como “skipped”. Esto es útil para tests que, aunque no requieren una DB, tardan segundos en ejecutarse (como tests de regresión de rendimiento o procesamiento de archivos grandes). La ventaja es que el archivo siempre se compila y el código está siempre disponible.TestIntegration_ExternalAPI: Este es el nivel más restrictivo. Gracias a//go:build integration, si ejecutasgo test ./..., el compilador ignora por completo este archivo. No hay coste de compilación ni de tiempo. Solo se incluye en el binario de pruebas cuando invocasgo test -tags integration. Esto es vital para evitar que el comando de test estándar falle en entornos de CI locales que no tengan acceso a la red o a servicios externos.
El error frecuente
Un error común es intentar usar //go:build en archivos que contienen lógica que el código de producción necesita.
// ERROR: Esto romperá tu aplicación en producción
//go:build integration
package database
func Connect() {
// ... lógica de conexión a DB
}
Si haces esto, cuando intentes construir tu binario de producción con go build ., el compilador lanzará un error porque el paquete database requerirá el tag integration para ser funcional. Los Build Tags deben usarse exclusivamente para archivos de test o para implementar variantes de código (como drivers de base de datos) que se elijan en el momento de la compilación del binario final mediante el uso de tags en la terminal.
N° 160