DEV Community

Cover image for Testing en Go, 2ª parte
Pedro Rojas Reyes
Pedro Rojas Reyes

Posted on • Updated on

Testing en Go, 2ª parte

Image description
Hoy les traigo la segunda parte de un tema que comenzamos a explorar en Medium. Si desean revisar la primera parte, les dejo el enlace aquí.

No tenía planeado escribir esta segunda parte inicialmente, pero me topé con un artículo que encontré muy interesante y que me inspiró a profundizar más en el tema. En esta continuación, nos adentraremos en los tipos de pruebas, las tablas de pruebas y los subtests en el contexto de Go.

Tipos de entradas de pruebas

  • Test positivos: Se prueban los casos en los que se espera que la función se ejecute correctamente y devuelva el resultado esperado. Este tipo de prueba se asegura de que la aplicación se comporte como se espera en condiciones normales. Los test positivos cubren lo siguiente:

    • Casos en los que las entradas son válidas.
    • Cómo se comporta la prueba en escenarios esperados.
    • Casos en los que se satisfacen los requisitos del sistema.
  • Test negativos: Se prueban los casos en los que se espera que la función falle o devuelva un resultado inesperado. Este tipo de prueba se asegura de que la aplicación se comporte correctamente con datos inválidos. Los test negativos cubren lo siguiente:

    • Casos en los que las entradas son inválidas.
    • Cómo se comporta la prueba en escenarios inesperados.
    • Cómo se comporta la prueba en escenarios fuera de los requisitos del sistema.

Ambos tipos de pruebas son igual de importantes para sistemas en producción. El manejo de errores es importante ya que se busca que los usuarios reciban una respuesta adecuada en caso de que algo falle, así como también se busca que el sistema se recupere exitosamente en caso de interrupciones o ralentizaciones.

Tablas de pruebas (table-driven)

Las tablas de pruebas son una forma de realizar pruebas en Go de manera más eficiente y ordenada. En lugar de escribir una prueba para cada caso, podemos definir una tabla con los casos de prueba y luego iterar sobre ella para ejecutar las pruebas.

Una tabla de test es como un test básico, excepto que mantiene una tabla de diferentes valores y resultados esperados. Los diferentes valores son iterados y se ejecuta el test para cada uno de ellos. Con cada iteración, el resultado es verificado con el resultado esperado. Esto ayuda a aprovechar una única función de prueba para probar un conjunto de diferentes valores y condiciones.

Retomando el ejemplo de la división que vimos en la primera parte, podemos definir una tabla de pruebas de la siguiente manera:

package main

import "testing"

func TestDivide2(t *testing.T) {
    var testTable = []struct {
        dividend int
        divisor  int
        expect int
        want  any  
    }{
        {10, 2, 5, 0},
        {8, 3, 2, 2},
        {7, 3, 2, 1},
        {10, 0, 0, nil}, // División por cero
    }

    for _, test := range testTable {
        result, err := Divide(test.dividend, test.divisor)
        if err == test.want {
            t.Errorf("Se esperaba un error al dividir por cero")
        }

        if result != test.expect {
            t.Errorf("Se esperaba %d pero se obtuvo %d", test.expect, result)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

En este caso, definimos una tabla de pruebas con los casos que queremos probar. Luego, iteramos sobre la tabla y ejecutamos la prueba para cada caso.
En los tres primeros casos, esperamos que la división sea exitosa y que el resultado sea el esperado. En el último caso, esperamos que la división falle y que se genere un error.

Al correr los tests vemos lo siguiente:
Resultado de pruebas

La desventaja de las tablas de pruebas es que si una prueba falla, no se ejecutan las pruebas restantes. Esto puede ser un problema si queremos ver todos los resultados de las pruebas, incluso si algunas fallan. Otra desventaja es que a medida que vas agregando más casos de prueba, la tabla de pruebas puede volverse difícil de leer y mantener. Para resolver esto, puedes dividir la tabla de pruebas en otras tablas más pequeñas o usar subtests.

Raramente he usado tablas de pruebas en mis proyectos. Recuerdo que recientemente las usé para probar casos similares donde esperaba resultados de error diferentes, pero en general no las uso mucho.

Creo que la importancia de las tablas de test radica en poder probar diferentes escenarios con una sola función de test, lo cual puede ser útil en ciertos casos.

Subtests

Este es mi tipo de pruebas favorito; los subtests son pruebas anidadas dentro de otra función de prueba. Los subtests son útiles cuando quieres probar diferentes escenarios o condiciones dentro de una sola función. Los subtests se ejecutan de forma independiente y si uno falla, los demás continúan ejecutándose.

Esta fue una característica que se introdujo en Go 1.7. La introducción de subtests permitió un mejor manejo de errores, un control detallado de pruebas a ejecutar desde la línea de comandos y a menudo da como resultado un código más limpio y más fácil de mantener.

El testing.T tiene un método llamado Run que se utiliza para crear subtests. El método Run toma dos parámetros:

func (t *T) Run(name string, f func(t *T)) bool
Enter fullscreen mode Exit fullscreen mode
  • name es el nombre del subtest.
  • f es la función de prueba que se ejecutará como subtest y recibe un *testing.T como argumento.

Una vez dentro del método Run, el motor de pruebas ejecutará la función de prueba y si falla, el subtest fallará. Si la función de prueba no falla, el subtest se considera exitoso. Esto nos permite establecer una estructura jerárquica de pruebas, cada una con su propio ámbito. Este enfoque nos permite construir jerarquías de pruebas en múltiples niveles según sea necesario.

Antes de pasar al ejemplo, hay un tip que tomé de un blog que compartieron en el trabajo y que me pareció interesante:

  • Usar .Parallel() para tests y subtests que no dependen entre sí. Esto permite que los tests se ejecuten en paralelo, lo que puede reducir el tiempo de ejecución de las pruebas.

Retomando el ejemplo de la división, podemos definir subtests de la siguiente manera:

func TestDivideWithSubTests(t *testing.T) {
    t.Parallel()

    actAssert := func(a, b, want int) {
        got, _ := Divide(a, b)
        if got != want {
            t.Errorf("Se esperaba %d pero se obtuvo %d", want, got)
        }
    }

    t.Run("positive input", func(t *testing.T) {
        t.Parallel()

        a, b := 10, 2
        want := 5
        actAssert(a, b, want)
    })

    t.Run("negative input", func(t *testing.T) {
        t.Parallel()

        a, b := -6, 0
        want := 0
        actAssert(a, b, want)
    })

    t.Run("undefined result", func(t *testing.T) {
        t.Parallel()

        a, b := 0, 10
        _, err := Divide(a, b)
        if err != nil {
            t.Errorf("Se esperaba un error al dividir por cero")
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

En este caso, definimos tres subtests dentro de la función de prueba TestDivideWithSubTests. Cada subtest prueba un escenario diferente y se ejecuta de forma independiente. Aquí he creado una función actAssert que se encarga de realizar la división y verificar el resultado. Luego, cada subtest llama a esta función con los valores de entrada y el resultado esperado. Estos son los casos que cubre cada subtest:

  • positive input: prueba la división con valores positivos.
  • negative input: prueba la división con valores negativos.
  • undefined result: prueba la división por cero.

Al correr el test, vemos lo siguiente:

go test -run TestDivideWithSubTests -v
=== RUN   TestDivideWithSubTests
=== PAUSE TestDivideWithSubTests
=== CONT  TestDivideWithSubTests
=== RUN   TestDivideWithSubTests/positive_input
=== PAUSE TestDivideWithSubTests/positive_input
=== RUN   TestDivideWithSubTests/negative_input
=== PAUSE TestDivideWithSubTests/negative_input
=== RUN   TestDivideWithSubTests/undefined_result
=== PAUSE TestDivideWithSubTests/undefined_result
=== CONT  TestDivideWithSubTests/positive_input
=== CONT  TestDivideWithSubTests/negative_input
=== CONT  TestDivideWithSubTests/undefined_result
--- PASS: TestDivideWithSubTests (0.00s)
    --- PASS: TestDivideWithSubTests/positive_input (0.00s)
    --- PASS: TestDivideWithSubTests/negative_input (0.00s)
    --- PASS: TestDivideWithSubTests/undefined_result (0.00s)
PASS
ok      github.com/Sirpyerre/listingposts_test/divide   0.001s
Enter fullscreen mode Exit fullscreen mode

Cuando ves los mensajes "PAUSE" y "CONT" en la salida de las pruebas de Go, eso indica que se están ejecutando pruebas en paralelo y Go está pausando y continuando la ejecución de las mismas.

Cuando ejecutas pruebas en paralelo, Go organiza la ejecución de las mismas de manera eficiente, pausando y continuando según sea necesario para garantizar que todas las pruebas se completen correctamente. Esto puede ayudar a optimizar el tiempo de ejecución, especialmente en sistemas con múltiples núcleos de CPU disponibles.

Cuando conocí los subtests por primera vez, me gustó mucho cómo se organizaban las pruebas y que además, con el IDE que uso (GoLand), puedo ver los subtests y ejecutarlos de manera independiente sin tener que ejecutar todo el test.


Bueno, es todo por esta segunda parte. Espero que les haya gustado y que les haya sido útil. Aún hay más cosas de las que podemos hablar sobre las pruebas en Go, como el paquete testify que es muy popular. Con este paquete podemos hacer pruebas de mocks, assertions, etc. Pero eso lo dejaremos para otra ocasión.

¡Me encantaría escucharte! ¿Qué te pareció este artículo? ¿Has tenido experiencias interesantes con pruebas en Go que te gustaría compartir? ¿O quizás tienes alguna sugerencia o mejora para este contenido? ¡Déjame un comentario abajo y hablemos!

¡Hasta la próxima y happy testing! 🧪🧪🧪

Referencias

Simion, A. (2019). Test-Driven Development in Go. Pragmatic Bookshelf.

Kennedy, W., Ketelsen, B., & St. Martin, E. (2015). Go in Action. Manning Publications.

The Go Authors. (2019, 17 de septiembre). Using Subtests and Sub-benchmarks. Recuperado de https://go.dev/blog/subtests

How I write tests in Go. [Blog post]. Recuperado de https://blog.verygoodsoftwarenotvirus.ru/posts/testing-in-go/index.html

Top comments (0)