Primeros pasos del testing en un proyecto Go
Tags: go, testing, architecture, spanish
Published: 2022-09-23 17:28:36

This article is my opinion and it follows the _[just sharing](https://justsharing.dev/) 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](#la-piramide)
* [El Sujeto](#el-sujeto)
* [Posibles soluciones](#soluciones)
* [Allanando el camino](#final)

## La pirámide <a name="la-piramide"></a>

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](https://martinfowler.com/bliki/TestPyramid.html). Cada _capa_ tiene un objetivo, un ciclo de feedback y validan diferentes partes del sistema.



![pirámide de test, unit abajo, acceptance arriba](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b1raxwwgo31yge6i5zbo.png)

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 <a name="el-sujeto"></a>

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.

```go
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.

{% embed https://dev.to/adyen/what-is-developer-experience-and-why-should-we-care-1k9i %}

## Posibles soluciones <a name="soluciones"></a>

### 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:

```sh
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
//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:
```go
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:

```go
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`:

```sh
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 <a name="final"></a>

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](https://www.pexels.com/photo/person-wearing-white-and-black-mid-rise-sneakers-at-borobudur-indonesia-346834/)