Introducción a Testcontainers para Go
Introducción
Testcontainers es una potente librería open source diseñada para realizar pruebas de integración en tus aplicaciones Go. La librería proporciona una forma sencilla de crear y gestionar contenedores Docker efímeros, permitiéndote simular dependencias como bases de datos, brokers de mensajería u otros servicios necesarios para el funcionamiento de tu aplicación durante la fase de pruebas. Mediante el uso de Testcontainers, puedes probar tu aplicación en un entorno que se asemeje a tu entorno de producción, reduciendo la necesidad de mocks o costosos entornos de pruebas.
En este artículo, te guiaré para que puedas comenzar a usar Testcontainers, centrándome en cómo incorporarlo a tu proyecto Go. Comenzaremos creando un nuevo proyecto Go y añadiendo la librería Testcontainers como dependencia. A continuación, te mostraré cómo crear e interactuar con un contenedor Docker que ejecuta un servidor HTTP como parte de tus pruebas.
Puedes encontrar el código fuente completo de este tutorial en GitHub.
Creando el proyecto
Comencemos creando un nuevo proyecto para explorar las posibilidades de Testcontainers.
Inicializando un nuevo módulo Go
Emplearemos Go Modules para gestionar nuestras dependencias. Para comenzar, crea un nuevo directorio e inicializa un nuevo módulo Go:
mkdir testcontainers-go-getting-started
cd testcontainers-go-getting-started
go mod init github.com/marcnuri-demo/testcontainers-go-getting-started
Si el proceso se completa correctamente, deberías tener un nuevo archivo go.mod
con el siguiente contenido:
module github.com/marcnuri-demo/testcontainers-go-getting-started
go 1.21.2
Creando el paquete main
A continuación, crearemos un nuevo archivo main_test.go
que nos servirá como la base para la implementación del test:
package main
import "testing"
func TestExample(t *testing.T) {
}
Añadiendo la dependencia de Testcontainers
Ahora que tenemos la estructura para el test, podemos añadir la librería Testcontainers como dependencia.
Para ello, emplearemos el comando go get
:
go get github.com/testcontainers/testcontainers-go
Este comando añadirá la dependencia de Testcontainers en tu archivo go.mod
y creará un archivo go.sum
con los checksums de las dependencias.
Implementando el test
Con la estructura del proyecto y la dependencia de Testcontainers en su lugar, podemos comenzar a trabajar en nuestro test.
Comenzaremos viendo cómo crear un nuevo contenedor que ejecute un sencillo servidor HTTP.
Creando un contenedor
Modifiquemos la función TestExample
para incluir una petición a Testconainers para crear un nuevo contenedor:
func TestExample(t *testing.T) {
containerCtx := context.Background()
chuckNorris, _ := testcontainers.GenericContainer(containerCtx, testcontainers.GenericContainerRequest{
Started: true,
ContainerRequest: testcontainers.ContainerRequest{
Image: "marcnuri/chuck-norris:latest",
ExposedPorts: []string{"8080/tcp"},
WaitingFor: wait.ForLog("Listening on: http://0.0.0.0:8080"),
},
})
defer func() {
err := chuckNorris.Terminate(containerCtx)
if err != nil {
panic(err)
}
}()
}
Veamos qué está sucediendo en el código anterior:
- Iniciamos un contexto Go llamado
containerCtx
que Testcontainers empleará para gestionar la información sobre el ciclo de vida del contenedor. - Usando la función
GenericContainer
, creamos un nuevo contenedor. Esta función requiere un contexto Go como primer argumento. El segundo parámetro es una estructuraGenericContainerRequest
con los siguientes campos:Started
: Un booleano que indica si el contenedor debe iniciarse tan pronto como se crea. Como queremos que el contenedor se inicie automáticamente, lo establecemos atrue
.ContainerRequest
: Una estructuraContainerRequest
que contiene la información sobre el contenedor que queremos crear. En este caso, queremos crear un nuevo contenedor usando la imagenmarcnuri/chuck-norris:latest
que expone una API HTTP en el puerto8080
.
Veamos con más detalle los campos de la estructura ContainerRequest
:
Image
: La imagen Docker que se usará para el contenedor.ExposedPorts
: Un array que especifica los puertos que se exponen desde el contenedor.WaitingFor
: Una estructuraLogStrategy
que se usará para esperar a que el contenedor esté listo. En este ejemplo, usamos la funciónForLog
para esperar a que el contenedor registre el mensajeListening on: http://0.0.0.0:8080
que es el mensaje que el contenedor traza cuando está listo para aceptar peticiones.
Por último, diferimos una función para asegurarnos de que el contenedor se termina después de que el test concluya, independientemente del resultado del test.
Puedes ejecutar el test con el siguiente comando:
go test
Si todo va bien, deberías ver una salida similar a la siguiente:
github.com/testcontainers/testcontainers-go - Connected to docker:
Resolved Docker Host: unix:///var/run/docker.sock
Resolved Docker Socket Path: /var/run/docker.sock
Test SessionID: 459a37cc7f20d8c0f04f777e23bcea0cfe0556ba695e226e643a700ed927b4b1
Test ProcessID: 52bf9bdd-48a2-4744-8f4e-52cfef44a17d
🐳 Creating container for image marcnuri/chuck-norris:latest
✅ Container created: a7a9ef65cf1a
🐳 Starting container: a7a9ef65cf1a
✅ Container started: a7a9ef65cf1a
🚧 Waiting for container id a7a9ef65cf1a image: marcnuri/chuck-norris:latest. Waiting for: &{timeout:<nil> Log:Listening on: http://0.0.0.0:8080 IsRegexp:false Occurrence:1 PollInterval:100ms}
🐳 Terminating container: a7a9ef65cf1a
🚫 Container terminated: a7a9ef65cf1a
Haciendo peticiones al contenedor
Ahora que tenemos un contenedor en ejecución, veamos cómo interactuar con él.
Modificaremos la función TestExample
para hacer una petición al contenedor y validar su respuesta:
func TestExample(t *testing.T) {
// ... Testcontainers initialization code
endpoint, err := chuckNorris.Endpoint(containerCtx, "http")
if err != nil {
t.Error(err)
}
response, err := http.Get(endpoint)
if err != nil {
t.Error(err)
}
if response.StatusCode != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode)
}
body, err := io.ReadAll(response.Body)
if err != nil {
t.Error(err)
}
bodyString := string(body)
if !strings.Contains(strings.ToLower(bodyString), "chuck") {
t.Errorf("Expected a Chuck Norris approved response, got \"%s\"", bodyString)
}
}
El fragmento de código anterior hace lo siguiente:
- Empleando la función
Endpoint
recuperamos el endpoint del contenedor. Esta función requiere un contexto Go como primer argumento y un protocolo como segundo argumento. En el ejemplo, queremos recuperar el endpoint HTTP, así que pasamoshttp
como segundo argumento. - Empleando la función
http.Get
, hacemos una petición al endpoint del contenedor. - Validamos que el código de estado de la respuesta es
200 OK
. - Leemos el cuerpo de la respuesta y validamos que contiene la palabra
Chuck
.
Puedes ejecutar el test de nuevo empleando el comando go test
.
Todo debería de funcionar como se espera y deberías ver el test en verde.
Conclusión
En este artículo, hemos visto cómo empezar a usar Testcontainers para Go desde cero. Hemos comenzado creando un nuevo proyecto Go y añadiendo la dependencia de Testcontainers. A continuación, hemos implementado un sencillo test que crea un nuevo contenedor y hace una petición HTTP al mismo. Ahora ya deberías de tener una sólida base para incorporar Testcontainers en tus proyectos Go.
Puedes encontrar el código fuente completo para este artículo en GitHub.
¡Feliz testing!