DEV Community

Cover image for Primeros pasos del testing en un proyecto Go
Juan Vega
Juan Vega

Posted on • Originally published at juandavidvega.es

Primeros pasos del testing en un proyecto Go

This article is my opinion and it follows the just sharing principle.

Aunque go trae un runner de test en su toolchain,go test que funciona efectivamente bien. Las necesidades de un proyecto mediano o grande van más allá de hacer simplemente go test ./... y que lo ejecute todo. Si los test no son cómodos de usar, lo más probable es que se acaben dejando de lado y se ejecuten solamente en el servidor de CI haciendo los desarrollos cada vez más lentos.

Table Of Contents

La pirámide

Aunque no voy a usar un naming estricto, para intentar guiar los ejemplos voy a hablar de diferentes tipologías de test, para entender porque es importante tenerlos separados hay que tener un poco en mente la pirámide de test. Cada capa tiene un objetivo, un ciclo de feedback y validan diferentes partes del sistema.

pirámide de test, unit abajo, acceptance arriba

Normalmente en otros lenguajes que he trabajado, suele haber (al menos) 3 grupos que se repiten siempre y que las diferentes herramientas soportan a aunque no siempre con el mismo nombre:

  • Acceptance (end to end): Test que van desde el punto más externo y expuesto del sistema hasta la parte más interna, ejemplo, Llamar al API expuesta para crear una entidad en la BBDD.
  • Integration: Testamos como algunas partes de nuestro sistema interactúan con elementos externos desde el punto de vista del sujeto del test. En ocasiones hay que incluso define integración dentro de un mismo proyecto, no voy a entrar en ese debate ahora mismo. Un ejemplo sería hacer un test del repositorio usando una base de datos en docker.
  • Unitarios: Igualmente no voy a entrar a definir que es la unidad, pero son los test que mejor aislados están desde el punto de vista del sujeto. Un ejemplo es tener un test que pruebe el algoritmo que usamos para calcular la puntuación de un partido.

Lo importante, más allá de definiciones para este post es tener claro que la jerarquía aquí la uso para definir que cada grupo tiene un ciclo de feedback más corto que el anterior, por ejemplo:

  • Acceptace: 10s
  • Integración: 3s
  • Unitarios: <100ms

Con esta premisa, la idea es tener una configuración que nos permita hacer uso de los diferentes niveles de información de forma cómoda y sin conflicto.

El sujeto

En los ejemplos me basaré en una función muy básica que simula una función para calcular el precio de un producto en función del stock que queda. He ignorado toda la gestión de errores por simplicidad.

func (calculator PriceCalculator) Calculate(product Product) float64 {
    stock := calculator.stockChecker.Check(product.ID)
    gap := stock.Gap()
    return product.BaseRate * gap
}
Enter fullscreen mode Exit fullscreen mode

Voy a tener tres suite de test diferentes, una donde uso un doble de prueba para el stockChecker, otra donde incluyo una implementación real que se conecta a la base de datos para stockChecker, y por ultima uno donde simulo llamar a un API HTTP que usa el PriceCalculator por debajo. El detalle de como escribo cada test lo dejo para otro post. En resumen tendríamos tres test:

  • TestPriceCalculator_Calculate
  • TestPriceCalculatorIntegration
  • TestPriceCalculatorAPI

Tenemos que asumir lo que comenté más arriba. Cada test tarda al menos el doble o el tripe que el anterior, siendo el de más arriba el más rápido. Tenemos que buscar la forma de dividir los ciclos de feedback para mejorar la Developer Experience.

Posibles soluciones

Convención de nombres

Una de las primeras opciones que me vino a la mente fue llegar a un consenso sobre el naming de los test. Hay soporte para esto en go test y lo uso habitualmente:

go test -run "regexp"
Enter fullscreen mode Exit fullscreen mode

La opción -run evalúa todo los test del proyecto y ejecuta solo los que coinciden con la expresión definida.

Esta solución es valida y como digo la uso habitualmente cuando quiero ejecutar solo un test (o un grupo) de toda la suite y estoy usando el terminal. El problema principal es que no hay prácticamente costumbre de esto en la comunidad ni en mi empresa actual e iba a ser algo que explicar a cada nueva persona que se uniese.

Incluso por error se podían acabar mezclando suites, un simple typo en la palabra Integration y podría hacer que un test se fuese a otro grupo.

Build tags

Si pones un comentario al inicio de fichero usando la palabra +build lo que haces es declarar un build tag que indican al copilador y al runner de test que ese fichero pertenece a un grupo concreto.

En nuestro caso sería poner al principio de cada fichero de test algo parecido a

//go:build integration
---Ambas notaciones `+build` y `go:build` son validas.
//+build acceptance
Enter fullscreen mode Exit fullscreen mode

Esta opción tiene algunos adeptos en la comunidad y se pueden encontrar post explicando como hay gente que los usa. El principal handicap para mi es que hay que tener bien configurado el IDE o puede resultar que tengas un test que no compila pero no te avisa porque esta fuera de las tags que esta mirando el IDE. Además, al igual que en la opción anterior, es sencillo dejar fuera un test por error o acabar con zombi code.

Cortos o largos, la solución de go test

La solución más común y la que finalmente he acabado adoptando por simplicidad a largo plazo. Valoro mucho tener algo que cualquier desarrollador Go puede heredar sin problemas.

Si no lo conoces entre los flags disponibles de go test tenemos -shot, ¿Qué hace -short veamoslo con un ejemplo?:

Si tenemos un test de integración:

func TestPriceCalculatorIntegration(t *testing.T) {
    // setup
    calculator := PriceCalculator{StockChecker: PostgreSQLStockChecker{}}
    // setup

    price := calculator.Calculate(Product{ID: "ASLSDFK000123"})

    // asserts
}
Enter fullscreen mode Exit fullscreen mode

Tanto el setup como la ejecución del mismo puede ser lenta así que manualmente lo marcamos:

func TestPriceCalculatorIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip()
    }

    // setup
    calculator := PriceCalculator{StockChecker: PostgreSQLStockChecker{}}
    // setup

    price := calculator.Calculate(Product{ID: "ASLSDFK000123"})

    // asserts
}
Enter fullscreen mode Exit fullscreen mode

testing.Short() nos permite saber si el flag -short ha sido incluido en el comando go test, de forma que para el siguiente comando sería true:

go test -race -short ./...
Enter fullscreen mode Exit fullscreen mode

De esta forma podemos generar dos ciclos de feedback, evidentemente esta opción tiene problemas:

  • Solo tienes dos niveles de test.
  • Alguien puede equivocarse al marcar un short
  • Por defecto, los test -short también se ejecutan al no incluir -short porque solo marcamos los lentos.

Aunque como menciono lo más relevante es que esta opción es la más común y la más conocida así que me parece la más eficaz.

Allanando el camino

Como hemos visto al final que test se ejecutan depende principalmente de los flag que hay en el comando go test, para facilitar esto normalmente utilizo Makefile de forma que simplemente con make test-unit o make test-integration se ejecuta el comando adecuado y el desarrollador no necesita recordar los flags.

Photo by Porapak Apichodilok from Pexels

Top comments (0)