Esta pagina se ve mejor con JavaScript habilitado

Go unit tests

 ·  🎃 kr0m

Tan importante como programar nuevas funcionalidades es asegurarse de que no se rompe nada en cada cambio, afortunadamente Go incluye todo lo necesario para ejecutar tests de forma rápida y cómoda.


Si eres nuevo en el mundo de Go te recomiendo los siguientes artículos anteriores:


Para conseguir una construcción reproducible Go recomienda utilizar versiones similares de Go, para ello podemos generar el fichero go.mod de forma manual o inicializar un módulo para que lo genere Go por nosotros:

mkdir go_testing
cd go_testing/
go mod init name

Podemos ver el contenido del fichero go.mod:

cat go.mod

module name

go 1.21.12

Construcción Reproducible:

La inclusión de un archivo go.mod en tu proyecto puede contribuir a la construcción reproducible. Esto significa que, al compartir tu código con
otros desarrolladores o al ejecutar pruebas en diferentes entornos, las mismas versiones de las dependencias se utilizarán, lo que puede ayudar
a evitar problemas de compatibilidad.

A modo de ejemplo simplemente tendremos una función llamada Hello que si le llega un string de entrada, devolverá [string,nil], si se le pasa una entrada vacía devolverá ["",ERROR]

vi name.go

package main

import (
    "errors"
    "fmt"
)

func hello(name string) (string, error) {
    if name == "" {
        return name, errors.New("Empty name")
    }
    return name, nil
}

func main() {
    name, error := hello("Kr0m")
    if error != nil {
        fmt.Println("ERROR:", error)
    } else {
        fmt.Println("Name:", name)
    }
}

Todos los ficheros de tests deben seguir el patrón XXX_test.go, se debe importar el package testing y las funciones deben empezar por Test.

Creamos el test unitario el cual comprobará que la función hello() devuelva un valor correcto,

vi name_test.go
package main

import (
    "testing"
)

// Test hello() function with input string:
func TestHelloName(t *testing.T) {
    // Function input
    name := "Kr0m"
    // What we expect
    want := "Kr0m"
    // Save hello() returned values
    msg, err := hello(name)
    // Compare want with msg OR an error has ocurred:
    if msg != want || err != nil {
        t.Fatalf(`hello(name) = %v, %v, want match for %v, nil`, msg, err, want)
    }
}

// Test hello() function with empty string:
func TestHelloEmpty(t *testing.T) {
    // Save hello() returned values
    msg, err := hello("")
    // If msg is not empty string OR err NULL:
    if msg != "" || err == nil {
        t.Fatalf(`hello("") = %v, %v, want "", error`, msg, err)
    }
}

Pasamos los tests sin problemas:

go test

PASS
ok  	name	0.002s

Si añadimos el parámetro -coverpodremos ver el porcentage de código que nuestros tests cubren:

go test -cover

PASS
coverage: 42.9% of statements
ok  	name	0.002s

Introducimos un bug, haciendo que la función siempre retorne [AAAA,nil]:

vi name.go

package main

import (
    "errors"
    "fmt"
)

func Hello(name string) (string, error) {
    if name == "" {
        return name, errors.New("Empty name")
    }
    name = "AAAA"           // BUG
    return name, nil
}

func main() {
    name, error := Hello("Kr0m")
    if error != nil {
        fmt.Println("ERROR:", error)
    } else {
        fmt.Println("Name:", name)
    }
}

Volvemos a pasar los tests:

go test

Podemos ver que esta vez ha fallado:

--- FAIL: TestHelloName (0.00s)
    name_test.go:17: hello(name) = AAAA, <nil>, want match for Kr0m, nil
FAIL
exit status 1
FAIL	name	0.002s

La línea 17 de name_test:

    if msg != want || err != nil {
        t.Fatalf(`hello(name) = %v, %v, want match for %v, nil`, msg, err, want)
    }

Efectivamente want no machea msg:

name := "Kr0m"
want := "Kr0m"
// msg -> AAAA
Si te ha gustado el artículo puedes invitarme a un RedBull aquí