Hoy es un día muy especial, y es que la mayoría de nosotros nos encontramos confinados en casa bajo el lema #QuédateEnCasa. La pandemia mundial que está significando el coronavirus (más técnicamente conocido como COVID-19) ha provocado que muchos estados hayan tenido que declarar el Estado de Alerta. Así que, como no podía ser menos, desde Friends of Go hemos decidido, pese a la excepcionalidad de la situación, no fallar a nuestro compromiso semanal.
Además, el de hoy es un artículo que, en cierto modo, encaja con la situación con la que nos encontramos. La situación actual está sirviendo para demostrar que muchos organismos e infraestructuras no estaban preparadas para una crisis de tal magnitud. Lo cuál, responde a la pregunta de: ¿ante que tipo de situaciones estamos preparados? En términos de programación, nos podríamos preguntar: ¿cuál es nuestra cobertura?
¿Qué es la cobertura o tests coverage?
Bien, pues como probablemente ya sabéis, cuándo hablamos de programación, las pruebas de software (especialmente las automatizadas) son el mecanismo más común para determinar si nuestros desarrollos funcionan como esperamos. Ya sean estas unitarias, de integración, aceptación, regresión o de cualquier otro tipo.
Y es, dentro de este tipo de prácticas, dónde nos encontramos con lo que comúnmente llamamos “tests coverage”, término que hace referencia a una métrica que, básicamente, nos viene a decir qué cantidad del código de nuestra aplicación se ha ejecutado durante la ejecución de dichas pruebas automatizadas. Entonces, lo lógico es pensar que el 100% de cobertura debería ser el objetivo a lograr, sin embargo, no siempre habrá suficiente con eso.
¿Cómo medimos la cobertura?
Como hemos introducido, la cobertura no deja de ser una métrica que nos va a indicar qué parte del código de nuestra aplicación ha sido ejecutada durante la ejecución de las pruebas automatizadas, sin embargo, esta métrica la podemos calcular de diferentes modos.
La primera de las estrategias, y probablemente una de las más comunes, es la de calcular la cobertura en base a las sentencias ejecutadas. Es decir, si nuestra aplicación tiene 1000 líneas efectivas de código y se han ejecutado solo 100, entonces nuestra cobertura será del 10%. Otro enfoque similar pero con otro nivel de granularidad es calcular la cobertura en base a las funciones. Es decir, si nuestra aplicación tiene 120 funciones y se ejecutan solo 60, entonces tendremos un 50% de cobertura.
Por otro lado, hay otros enfoques a la hora de calcular la cobertura, basados en el número de caminos críticos de nuestra
aplicación. En primer lugar, el conocido como branch coverage se basa en el número de ramas de nuestro código ejecutadas,
entendiendo cada rama como cada uno de los distintos caminos de ejecución que genera una sentencia condicional (if
, switch
, etc).
Y, en segundo lugar, el conocido como condition coverage se basa en el número de ejecuciones para cada expresión booleana,
es decir, si cada una de estas ha sido ejecutada en su condición cierta y en su condición falsa.
La cobertura en Go
Ahora que ya tenemos clara esa métrica y las diferentes maneras que tenemos de calcularla, es hora de ver cómo obtenerla cuándo estamos desarrollando en Go. Para ello, vamos a usar la librería clog, que pese a que aún está en desarrollo es un ejemplo más que adecuado para esta situación. Además, con el propósito de simplificar la comprensión del artículo, nos vamos a centrar en lo que conocemos como statements coverage, que anteriormente describimos como el porcentaje de sentencias (o líneas efectivas) de código que se han ejecutado.
Usando el flag -cover
Como ya deberíamos saber, la herramienta go test
nos proporciona un flag para hacer una ejecución de los tests unitarios
y obtener la métrica de cobertura de dicha ejecución. Veamos la salida del ejemplo mencionado:
╰─ go test ./... -cover ─╯
ok github.com/friendsofgo/clog 0.006s coverage: 100.0% of statements
? github.com/friendsofgo/clog/reporter [no test files]
? github.com/friendsofgo/clog/reporter/log [no test files]
? github.com/friendsofgo/clog/reporter/mock [no test files]
Con este primer paso, hemos podido obtener la métrica de cobertura para cada uno de los paquetes de nuestra aplicación.
Sin embargo, el flag -cover
no nos servirá para obtener la cobertura total de nuestra aplicación.
Calculando la mediana aritmética
Un primer enfoque para calcular la cobertura total, aunque no muy preciso, puede consistir en calcular la mediana aritmética de los resultados que nos devolvió el comando anterior. Es decir, si nuestra librería está dividida en cuatro paquetes, lo que haremos será sumar la cobertura de cada uno de esos paquetes y luego dividiremos el total entre cuatro.
Para ello, lo primero será obtener la suma total de las coberturas de los diferentes paquetes:
╰─ go test ./... --cover | awk '{if ($1 != "?") print $5; else print "0.0";}' | sed 's/\%//g' | awk '{s+=$1} END {printf "%.2f\n", s}' ─╯
100.00
En este caso, lo que obtendremos será un 100% porqué tenemos un paquete con una cobertura del 100% y el resto de paquetes sin cubrir. Posteriormente, lo que deberíamos hacer es obtener el número total de paquetes:
╰─ go test ./... --cover | wc -l ─╯
4
Finalmente, con una simple división podríamos obtener el porcentaje de cobertura de nuestra aplicación (100% / 4) = 25%
.
Sin embargo, como es fácil de apreciar, este indicador no nos servirá de mucho, pues está considerando todos los paquetes por igual.
Con lo cuál, si tenemos una cobertura mayor en paquetes muy grandes, vamos a obtener un valor bastante más bajo del real,
por contra, si tenemos una mayor cobertura en paquetes pequeños, entonces vamos a obtener un valor bastante más alto del real,
y lo que es peor, teniendo una falsa sensación de seguridad.
Usando la herramienta go tool cover
Otro posible enfoque, y quizás el más popular, es el de usar la herramienta go tool cover
. Esta herramienta nos va a
permitir darle como entrada un “perfil de cobertura” a partir del cuál calculará la cobertura total de la aplicación.
Para ello, lo primero que necesitamos es obtener ese “perfil de cobertura”, el cuál podemos obtener con el siguiente script:
PKG_LIST=$(go list ./... | grep -v /vendor/ | tr '\n' ' ')
go test -covermode=count -coverprofile coverage $PKG_LIST
Como podéis ver, lo que estamos haciendo básicamente es:
- Por un lado, obtener el listado de todos los paquetes de nuestra aplicación / librería (excluyendo las dependencias).
- Y, por otro lado, generar ese “perfil de cobertura”, que será guardado en el fichero
coverage
.
Finalmente, tal y como indicamos anteriormente, lo que haremos será ejecutar la herramienta go tool cover
:
╰─ go tool cover -func=coverage
github.com/friendsofgo/clog/context.go:11: newContext 100.0%
github.com/friendsofgo/clog/context.go:15: lineFromContext 100.0%
github.com/friendsofgo/clog/line.go:29: AddTag 100.0%
github.com/friendsofgo/clog/line.go:36: AddTags 100.0%
github.com/friendsofgo/clog/line.go:45: OpenSpan 100.0%
github.com/friendsofgo/clog/line.go:61: MarkAsInfo 100.0%
github.com/friendsofgo/clog/line.go:69: MarkAsError 100.0%
github.com/friendsofgo/clog/line.go:77: MarkAsCritical 100.0%
github.com/friendsofgo/clog/line.go:85: Send 100.0%
github.com/friendsofgo/clog/line.go:89: send 100.0%
github.com/friendsofgo/clog/line.go:96: format 100.0%
github.com/friendsofgo/clog/logger.go:22: New 100.0%
github.com/friendsofgo/clog/logger.go:35: WithReporters 100.0%
...
github.com/friendsofgo/clog/tag.go:125: Float64 100.0%
github.com/friendsofgo/clog/tag.go:130: Uint 100.0%
github.com/friendsofgo/clog/tag.go:135: Uint8 100.0%
github.com/friendsofgo/clog/tag.go:140: Uint16 100.0%
github.com/friendsofgo/clog/tag.go:145: Uint32 100.0%
github.com/friendsofgo/clog/tag.go:150: Uint64 100.0%
github.com/friendsofgo/clog/tag.go:155: Error 100.0%
github.com/friendsofgo/clog/tag.go:163: Skip 100.0%
github.com/friendsofgo/clog/tag.go:168: Any 100.0%
total: (statements) 100.0%
Como podéis ver, ahora sí tendremos el statements coverage total, ponderado adecuadamente en función del número de
sentencias que contenga cada paquete, a diferencia del enfoque anterior. Sin embargo, aún seguimos teniendo un problema.
De hecho, si os fijáis en el informe de cobertura de la librería que estamos usando como ejemplo, váis a ver que
nos indica una cobertura del 100%,
tal y como vimos en la salida de la herramienta go tool cover
. Sin embargo, eso no es cierto. Pues, como vimos anteriormente,
tres de los cuatro paquetes de la aplicación no tienen cobertura.
Añadiendo ficheros de test a todos los paquetes
Ahora que ya hemos visto cuál es el enfoque más adecuado para calcular la cobertura de nuestras aplicaciones de forma correcta, solo nos falta por solucionar el aspecto que comentamos al final del apartado anterior: calcular la cobertura ponderada de los paquetes que no tienen tests. Para ello, vamos a crear un fichero de test en cada paquete.
El objetivo es extremadamente sencillo. Como nuestro problema reside en que el “perfil de cobertura” no incluye datos para
los paquetes que no tienen ficheros de test (*_test.go
), lo que vamos a hacer será crear un fichero de test en cada uno
de los paquetes que no cumplan esa condición. El nombre de dichos ficheros es poco relevante, y con un fichero es suficiente.
Sin embargo, por coherecia, en nuestro ejemplo
hemos añadido un fichero de test para cada uno de los ficheros de código
que aún no tuviera tests.
¡Y ahora sí! Ya tenemos nuestra cobertura total real:
╰─ go tool cover -func=coverage
github.com/friendsofgo/clog/context.go:11: newContext 100.0%
github.com/friendsofgo/clog/context.go:15: lineFromContext 100.0%
...
github.com/friendsofgo/clog/logger.go:70: send 100.0%
github.com/friendsofgo/clog/reporter/log/log.go:15: NewReporter 0.0%
github.com/friendsofgo/clog/reporter/log/log.go:23: Send 0.0%
github.com/friendsofgo/clog/reporter/log/log.go:27: Close 0.0%
github.com/friendsofgo/clog/reporter/mock/reporter.go:15: Send 0.0%
github.com/friendsofgo/clog/reporter/mock/reporter.go:20: Close 0.0%
github.com/friendsofgo/clog/reporter/reporter.go:28: NewNoop 0.0%
github.com/friendsofgo/clog/reporter/reporter.go:32: Send 0.0%
github.com/friendsofgo/clog/reporter/reporter.go:33: Close 0.0%
github.com/friendsofgo/clog/span.go:36: StartTransaction 100.0%
...
github.com/friendsofgo/clog/tag.go:163: Skip 100.0%
github.com/friendsofgo/clog/tag.go:168: Any 100.0%
total: (statements) 91.6%
De este modo, también podemos, de un modo sencillo, decidir qué paquetes no queremos que sean tenidos en cuenta (ignorados) para el cálculo total de la cobertura de nuestra aplicación o librería.
Y vosotros, ¿estáis calculando la cobertura de vuestras aplicaciones y librerías de forma correcta?
Como ya sabéis, todos los comentarios, feedback o las dudas que hayan quedado por resolver, serán bienvenidas en la sección de comentarios del artículo o en nuestro Twitter @FriendsofGoTech.