1. Code
  2. Coding Fundamentals

Let's Go: Probando programas Golang

En este tutorial te enseñaré todos los conceptos básicos de las pruebas idiomáticas en Go utilizando las mejores prácticas desarrolladas por los diseñadores de idiomas y la comunidad. El arma principal será el paquete de pruebas estándar. El objetivo será un programa de muestra que resuelve un problema simple del Proyecto Euler.
Scroll to top

Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)

En este tutorial te enseñaré todos los conceptos básicos de las pruebas idiomáticas en Go utilizando las mejores prácticas desarrolladas por los diseñadores de idiomas y la comunidad. El arma principal será el paquete de pruebas estándar. El objetivo será un programa de muestra que resuelve un problema simple del Proyecto Euler.

Diferencia de suma cuadrada

El problema de la diferencia de suma cuadrada es bastante simple: "Encuentra la diferencia entre la suma de los cuadrados de los primeros cien números naturales y el cuadrado de la suma".

Este problema en particular se puede resolver de forma bastante concisa, especialmente si conoces a tu Gauss. Por ejemplo, la suma de los primeros números naturales N es (1 + N) * N / 2, y la suma de cuadrados de los primeros enteros N es: (1 + N) * (N * 2 + 1) * N / 6. Así que todo el problema se puede resolver con la siguiente fórmula y asignando 100 a N:

(1 + N) * (N * 2 + 1) * N / 6 - ((1 + N) * N / 2) * ((1 + N) * N / 2)

Bueno, eso es muy específico, y no hay mucho que probar. En cambio, creé algunas funciones que son un poco más generales de lo que se necesita para este problema, pero que pueden servir para otros programas en el futuro (el proyecto Euler tiene 559 problemas en este momento).

El código está disponible en GitHub.

Estas son las firmas de las cuatro funciones:

1
// The MakeIntList() function returns an array of consecutive integers
2
3
// starting from 1 all the way to the `number` (including the number)
4
5
func MakeIntList(number int) []int
6
7
8
9
// The squareList() function takes a slice of integers and returns an
10
11
// array of the quares of these integers
12
13
func SquareList(numbers []int) []int
14
15
16
17
// The sumList() function takes a slice of integers and returns their sum
18
19
func SumList(numbers []int) int
20
21
22
23
// Solve Project Euler #6 - Sum square difference
24
25
func Process(number int) int

Ahora, con nuestro programa objetivo en su lugar (por favor perdónenme, fanáticos TDD), veamos cómo escribir pruebas para este programa.

El paquete de pruebas

El paquete de pruebas va de la mano con el comando go test. Las pruebas de tu paquete deben ir en archivos con el sufijo "_test.go". Puedes dividir tus pruebas en varios archivos que sigan esta convención. Por ejemplo: "whatever1_test.go" y "whatever2_test.go". Debes colocar tus funciones de prueba en estos archivos de prueba.

Cada función de prueba es una función exportada públicamente cuyo nombre comienza con "Test", acepta un puntero a un objeto testing.T, y no devuelve nada. Se ve así:

1
func TestWhatever(t *testing.T) {
2
    // Your test code goes here
3
} 

El objeto T proporciona varios métodos que puedes utilizar para indicar fallas o registrar errores.

Recuerda: solo las funciones de prueba definidas en los archivos de prueba serán ejecutadas por el comando go test.

Pruebas de escritura

Cada prueba sigue el mismo flujo: configura el entorno de prueba (opcional), ingresa el código bajo la entrada de prueba, captura el resultado y compáralo con la salida esperada. Ten en cuenta que las entradas y los resultados no tienen que ser argumentos para una función.

Si el código sometido a prueba está obteniendo datos de una base de datos, la entrada se asegurará de que la base de datos contenga los datos de prueba adecuados (lo que puede implicar simulaciones en varios niveles). Pero, para nuestra aplicación, el escenario común de pasar argumentos de entrada a una función y comparar el resultado con la salida de la función es suficiente.

Comencemos con la función SumList(). Esta función toma una porción de números enteros y devuelve su suma. Esta es una función de prueba que verifica que SumList() se comporte como debería.

Prueba dos casos de prueba y, si una salida esperada no coincide con el resultado, llama al método Error() del objeto testing.T.

1
func TestSumList_NotIdiomatic(t *testing.T) {
2
    // Test []{} -> 0
3
    result := SumList([]int{})
4
    if result != 0 {
5
            t.Error(
6
                "For input: ", []int{},
7
                "expected:", 0,
8
                "got:", result)
9
    }
10
11
    // Test []{4, 8, 9} -> 21
12
    result = SumList([]int{4, 8, 9})
13
    if result != 21 {
14
            t.Error(
15
                "For input: ", []int{},
16
                "expected:", 0,
17
                "got:", result)
18
    }
19
}

Todo esto es sencillo, pero parece un poco detallado. Las pruebas idiomáticas de Go utilizan pruebas basadas en tablas en las que defines una estructura para pares de entradas y salidas esperadas y luego tiene una lista de estos pares que alimentas en un bucle a la misma lógica. Así es como se prueba la función SumList().

1
type List2IntTestPair struct {
2
    input  []int
3
    output int
4
}
5
6
7
func TestSumList(t *testing.T) {
8
    var tests = []List2IntTestPair{
9
        {[]int{}, 0},
10
        {[]int{1}, 1},
11
        {[]int{1, 2}, 3},
12
        {[]int{12, 13, 25, 7}, 57},
13
    }
14
15
    for _, pair := range tests {
16
        result := SumList(pair.input)
17
        if result != pair.output {
18
            t.Error(
19
                "For input: ", pair.input,
20
                "expected:", pair.output,
21
                "got:", result)
22
        }
23
    }
24
}

Mucho mejor. Es fácil agregar más casos de prueba. Es fácil tener el espectro completo de casos de prueba en un solo lugar, y si decides cambiar la lógica de prueba, no necesitas cambiar varias instancias.

Este es otro ejemplo para probar la función SquareList(). En este caso, tanto la entrada como la salida son porciones de números enteros, por lo que la estructura del par de prueba es diferente, pero el flujo es idéntico. Una cosa interesante aquí es que Go no proporciona una forma incorporada de comparar segmentos, por lo que utilizo reflect.DeepEqual() para comparar el segmento de salida con el segmento esperado.

1
type List2ListTestPair struct {
2
    input  []int
3
    output []int
4
}
5
6
func TestSquareList(t *testing.T) {
7
    var tests = []List2ListTestPair{
8
        {[]int{}, []int{}},
9
        {[]int{1}, []int{1}},
10
        {[]int{2}, []int{4}},
11
        {[]int{3, 5, 7}, []int{9, 25, 49}},
12
    }
13
14
    for _, pair := range tests {
15
        result := SquareList(pair.input)
16
        if !reflect.DeepEqual(result, pair.output) {
17
            t.Error(
18
                "For input: ", pair.input,
19
                "expected:", pair.output,
20
                "got:", result)
21
        }
22
    }
23
}

Ejecución de pruebas

Ejecutar pruebas es tan simple como escribir go test en el directorio del paquete. Go encontrará todos los archivos con el sufijo "_test.go" y todas las funciones con el prefijo "Test" y las ejecutará como pruebas. Así se ve cuando todo está bien:

1
(G)/project-euler/6/go > go test

2


3
PASS
4
5
ok      _/Users/gigi/Documents/dev/github/project-euler/6/go	0.006s

No es muy dramático. Déjame hacer una prueba a propósito. Cambiaré el caso de prueba para SumList() de modo que la salida esperada para sumar 1 y 2 sea 7.

1
func TestSumList(t *testing.T) {
2
    var tests = []List2IntTestPair{
3
        {[]int{}, 0},
4
        {[]int{1}, 1},
5
        {[]int{1, 2}, 7},
6
        {[]int{12, 13, 25, 7}, 57},
7
    }
8
9
    for _, pair := range tests {
10
        result := SumList(pair.input)
11
        if result != pair.output {
12
            t.Error(
13
                "For input: ", pair.input,
14
                "expected:", pair.output,
15
                "got:", result)
16
        }
17
    }
18
}

Ahora, cuando escribes go test, obtienes:

1
(G)/project-euler/6/go > go test
2
3
--- FAIL: TestSumList (0.00s)
4
5
006_sum_square_difference_test.go:80: For input:  [1 2] expected: 7 got: 3
6
7
FAIL
8
9
exit status 1
10
11
FAIL    _/Users/gigi/Documents/dev/github/project-euler/6/go	0.006s

Eso afirma bastante bien lo que sucedió y debe darte toda la información que necesitas para solucionar el problema. En este caso, el problema es que la prueba en sí es incorrecta y el valor esperado debe ser 3. Es una lección importante. No supongas automáticamente que si se produce un error en una prueba, el código sometido a prueba está roto. Ten en cuenta todo el sistema, lo que incluye el código en pruebas, la prueba en sí y el entorno de prueba.

Cobertura de pruebas

Para asegurarte de que tu código funciona, no basta con realizar pruebas. Otro aspecto importante es la cobertura de pruebas. ¿Tus pruebas cubren todas las instrucciones del código? A veces ni siquiera es suficiente. Por ejemplo, si tienes un bucle en el código que se ejecuta hasta que se cumple una condición, puedes probarla correctamente con una condición que funcione, pero no te das cuenta de que en algunos casos la condición siempre puede ser falsa, lo que da como resultado un bucle infinito.

Pruebas unitarias

Las pruebas unitarias son como cepillarse los dientes y usar hilo dental. No deberías descuidarlos. Son la primera barrera contra los problemas y te permitirán tener confianza en la refactorización. También son una bendición al intentar reproducir problemas y poder escribir una prueba con errores que muestra el problema que pasa después de solucionarlo.

Pruebas de integración

Las pruebas de integración también son necesarias. Piensa en ellas como una visita al dentista. Puedes estar bien sin ellos por un tiempo, pero si los descuidas por mucho tiempo no será agradable.

La mayoría de los programas no triviales están hechos de múltiples módulos o componentes relacionados entre sí. A menudo, pueden producirse problemas al conectar esos componentes. Las pruebas de integración te dan confianza de que todo el sistema funciona según lo previsto. Hay muchos otros tipos de pruebas, como pruebas de aceptación, pruebas de rendimiento, pruebas de estrés/carga y pruebas completas del sistema completo, pero las pruebas unitarias y las pruebas de integración son dos de las formas fundamentales de probar el software.

Conclusión

Go tiene soporte integrado para pruebas, una forma bien definida de escribir pruebas y directrices recomendadas en forma de pruebas basadas en tablas.

La necesidad de escribir estructuras especiales para cada combinación de entradas y salidas es un poco molesta, pero ese es el precio que pagas por el enfoque simple de diseño de Go.