Intro

Testing package

El ecosistema de Go trae soporte nativo para escribir y ejecutar tests automáticos. El paquete testing es el que provee soporte para la escritura de tests.

Unit test

Para definir una test suite es necesario que el fichero termine en: _test.go. Definir un test case se tiene que escribir una función con la forma:

func TestXxx(t *testing.T)

Donde Xxx es el nombre del caso de prueba y debe comenzar con mayúsculas.

Para correr los tests basta con ejecutar go test y automáticamente buscará las suites y correrá los tests.

go test

Pongamos un ejemplo sencillo para ilustrar los conceptos. Supongamos un paquete que se encarga de realizar operaciones matemáticas: sumas, restas, multiplicaciones y divisiones con su respectivo test.

  • math.go
package main

func Add(a, b int) int {
    return a + b
}
  • math_test.go
package main

import "testing"

func TestAddZeroValues(t *testing.T) {
    res := Add(0, 0)
    if res != 0 {
        t.Errorf("Expected 0, got %d\n", res)
    }
}

func TestSubZeroValues(t *testing.T) {
    res := Sub(0, 0)
    if res != 0 {
        t.Errorf("Expected 0, got %d\n", res)
    }
}

Por último para ejecutar el test y mostrar la salida de cada test:

go test -v

Table-Driven test

El table-driven test es un concepto para la escritura de tests que consiste en escribir una matriz que contenga las entradas y salidas de los esperadas para un caso de prueba. Esta técnica nos evita tener que escribir múltiples casos de prueba para diferentes entradas.

func TestTableDrivenDiv(t *testing.T) {
    var table = []struct {
        name string
        input_a int
        input_b int
        expected int
    }{
        {"Divide 1/1", 1, 1, 1},
        {"Divide -1/1", -1, 1, -1},
        {"Divide 0/1", 0, 1, 0},
        {"Divide 2/1", 2, 1, 2},
    }
    for _, test := range table {
        t.Run(test.name, func(t *testing.T) {
            res := Div(test.input_a, test.input_b)
            if res != test.expected {
                t.Errorf("Expected %d, got %d\n", test.expected, res)
            }
        })
    }
}

Coverage

Se puede conocer el porcentaje de cobertura de nuestro código con go test:

go test -cover

Benchmark

Para escribir benchmarks:

Las funciones con la forma:

func BenchmarkXxx(t *testing.B)

Se consideran benchmarks

Un ejemplo de Benchmark:

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}

Para correr un benchmark se tiene que correr explícitamente con:

go test -bench=.

Fuzz test

Fuzzy testing es una técnica de testing que consiste en introducir entradas no esperadas al elemento de prueba.

Go también provee de esta funcionalidad de forma nativa. Para definir un Fuzz test:

func FuzzXxx (f *testing.F)

Para ilustrarlo, remotomemos el ejemplo anterior y creemos un Fuzz test para la función Div:

func FuzzDiv(f *testing.F) {
    f.Fuzz(func (t *testing.T, a, b int) {
        Div(a, b)
    })
}

Como se puede ver f.Fuzz acepta múltiples argumentos para usar como entrada a nuestro caso de prueba.

Deliberadamente he dejado que el denominador pueda tener el valor cero y por lo tanto la función Div contenga un error. El Fuzz test va a conseguir reproducir esa condición de error. Para ejecutarlo y ver la salida:

$ go test -fuzz=.

fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed
failure while testing seed corpus entry: FuzzDiv/7bef6c8ac710a3fe
fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed
--- FAIL: FuzzDiv (0.01s)
    --- FAIL: FuzzDiv (0.00s)
        testing.go:1485: panic: runtime error: integer divide by zero
            goroutine 20 [running]:
            runtime/debug.Stack()
                /usr/lib/go/src/runtime/debug/stack.go:24 +0x9e
            testing.tRunner.func1()
                /usr/lib/go/src/testing/testing.go:1485 +0x1f6
            panic({0x5c32e0, 0x721fc0})
                /usr/lib/go/src/runtime/panic.go:884 +0x213
            test.Div(...)
                /tmp/test/math.go:16
            test.FuzzDiv.func1(0x0?, 0x0?, 0x0?)
                /tmp/test/math_test.go:64 +0x3d
            reflect.Value.call({0x5bf720?, 0x5fc668?, 0x13?}, {0x5ed030, 0x4}, {0xc0000a01e0, 0x3, 0x4?})
                /usr/lib/go/src/reflect/value.go:586 +0xb0b
            reflect.Value.Call({0x5bf720?, 0x5fc668?, 0x6f31a0?}, {0xc0000a01e0?, 0x5ec720?, 0xc00009a058?})
                /usr/lib/go/src/reflect/value.go:370 +0xbc
            testing.(*F).Fuzz.func1.1(0x0?)
                /usr/lib/go/src/testing/fuzz.go:335 +0x3f3
            testing.tRunner(0xc0000824e0, 0xc0000b0090)
                /usr/lib/go/src/testing/testing.go:1576 +0x10b
            created by testing.(*F).Fuzz.func1
                /usr/lib/go/src/testing/fuzz.go:322 +0x5b9


FAIL
exit status 1
FAIL    test    0.008s

Testify

Testify es un paquete de pruebas unitarias de Go que proporciona funciones adicionales y mejoras a la biblioteca estándar de pruebas de Go. Testify usa una sintaxis similar a otros frameworks de pruebas de otros lenguajes de programación.

Por ejemplo testify provee del paquete assert que provee una sintaxis legible para hacer nuestros tests como:

assert.Equal()
assert.True()
assert.Nil()

Para instalar testify:

go get github.com/stretchr/testify

Usando testify en nuestro proyecto:

package main

import (
        "github.com/stretchr/testify/assert"
        "testing"
       )

func TestTableDrivenDiv(t *testing.T) {
    var table = []struct {
        name     string
            input_a  int
            input_b  int
            expected int
    }{
        {"Divide 1/1", 1, 1, 1},
            {"Divide -1/1", -1, 1, -1},
            {"Divide 0/1", 0, 1, 0},
            {"Divide 2/1", 2, 1, 2},
    }
    for _, test := range table {
        t.Run(test.name, func(t *testing.T) {
                assert.Equal(t, Div(test.input_a, test.input_b), test.expected, "")
                })
    }
}

https://pkg.go.dev/testing

https://blog.jetbrains.com/go/2022/11/22/comprehensive-guide-to-testing-in-go/#WritingFuzzTests