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.
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
}
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.
What is Developer Experience and why should we care?
Deepu K Sasidharan for Adyen ・ Jul 16 '21 ・ 6 min read
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"
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
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
}
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
}
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 ./...
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.
Top comments (0)