Introducción: Por qué las pruebas avanzadas son un must have en Go
Go (Golang) es conocido por su simplicidad y sus herramientas integradas para pruebas. Sin embargo, cuando un proyecto crece más allá del "Hello, World", el estándar go test deja de ser suficiente. A nivel avanzado, los desarrolladores se enfrentan a preguntas como: cómo aislar correctamente las pruebas, cómo probar el trabajo con bases de datos, cómo simular servicios externos y cómo organizar una verificación integral de toda la aplicación.
En este artículo, analizaremos tres niveles de pruebas — Unit, Integration y E2E — con ejemplos de la práctica real. Aprenderá a usar mocks, contenedores para pruebas de integración y a construir un pipeline fiable para CI/CD. El material está dirigido a desarrolladores que ya conocen los fundamentos de Go y desean llevar la calidad de su código al siguiente nivel.
1. Pruebas unitarias: aislamiento, mocks y el patrón Table-Driven Tests
Las pruebas unitarias verifican el funcionamiento de un solo módulo (función, método) en completo aislamiento de las dependencias externas. En Go, para esto se suelen usar interfaces y bibliotecas de mocks, como testify/mock o gomock.
1.1. Interfaces como base de las pruebas
Para reemplazar un servicio real por un mock, el código debe depender de una interfaz, no de una implementación concreta. Veamos un ejemplo de un servicio para enviar notificaciones.
// notifier.gotype Notifier interface { Send(to string, message string) error}
type UserService struct { Notifier Notifier}
func (s *UserService) NotifyUser(userID string) error { // lógica para obtener el email del usuario email := userID + "@example.com" return s.Notifier.Send(email, "Welcome!")}1.2. Creación de un mock con testify/mock
Instalamos la biblioteca: go get github.com/stretchr/testify.
// notifier_mock.go (en el paquete user_test)type NotifierMock struct { mock.Mock}
func (m *NotifierMock) Send(to string, message string) error { args := m.Called(to, message) return args.Error(0)}1.3. Table-Driven Tests
Este es un patrón estándar en Go para probar múltiples casos. Mejora la legibilidad y simplifica la adición de nuevas pruebas.
// user_service_test.gofunc TestNotifyUser(t *testing.T) { tests := []struct { name string userID string mockError error expectErr bool }{ {"success", "user1", nil, false}, {"fail_send", "user2", errors.New("send error"), true}, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockNotifier := new(NotifierMock) service := &UserService{Notifier: mockNotifier}
mockNotifier.On("Send", tt.userID+"@example.com", "Welcome!").Return(tt.mockError)
err := service.NotifyUser(tt.userID) if tt.expectErr { assert.Error(t, err) } else { assert.NoError(t, err) } mockNotifier.AssertExpectations(t) }) }}Best practice: Use t.Run para subpruebas — esto permite ejecutar cada caso por separado y ver cuál falló exactamente.
2. Pruebas de integración: trabajo con BD y servicios externos
Las pruebas de integración verifican la interacción de varios componentes: código Go con la base de datos, servidor API con la caché, etc. El principal desafío es gestionar el estado del entorno. Aquí es donde entran en juego los Testcontainers.
2.1. Testcontainers para Go
Testcontainers permiten levantar contenedores Docker directamente desde las pruebas. Esto proporciona un entorno limpio y aislado para cada ejecución.
Instalación: go get github.com/testcontainers/testcontainers-go