Aprendiendo Go con TDD

Hace tiempo que quiero hincar el diente a Go, el lenguaje de Google. Empecé jugando un poco con exercism.io y creo que es hora de empezar Go con test. Vamos a intentar explicar como testear un ejemplo sencillo con Go, para más adelante evolucionar ese ejemplo con usando un enfoque de TDD. La idea es conocer como funciona el lenguaje: declarar variables, crear funciones, hacer test, usar constantes…

5803193649_18e29b5b0e_z
Micah Camara – Go

Empezando

Lo primero es instalar Go, aquí tenemos un tutorial que hice sobre como instalar go en Mac.Vamos a utilizar el IDE de IntelliJ Goland que es lo más cómodo si ya estamos acostumbrados a PHPStorm.

Go es un poco especial para el workspace. En Mac por defecto se define dentro de nuestro usuario en una carpeta go y ahí irá añadiendo todas las dependencias, paquetes y movidas. Lo ideal es que tengamos estas variables de entorno definidas para que todo funcione bien.

export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
export GOBIN=$GOPATH/bin

Por otro lado gestionar dependencias en go…. vamos lo que viene siendo trabajar con librerias, versiones y tal es un poco “especial” en este lenguaje. Así que para empezar lo ideal es crear esta ruta para empezar a trabajar “~/go/src/github.com//golang-examples” donde username es tu usuario de github.

Recapitulando: Go instalado, variables de entorno creadas, carpeta del proyecto “golang-examples” de ejemplo lista.

Hello World

Tradicionalmente el primer ejemplo que usamos para empezar con un lenguaje es escribir por pantalla “Hello World”.

Todo el código del proyecto podemos encontrarlo dentro de este repositorio de GitHub https://github.com/jeslopcru/golang-examples/tree/master

Así que allá vamos:

  • Creamos una carpeta hello dentro de nuestro proyecto “golang-examples”
  • ahí creamos el archivo hello.go
package main​
import "fmt"func main() {
    fmt.Println("Hello, world")
}

Lo guardamos y para ver si todo funciona ha ejecutamos

$ go run hello/hello.go
Hello, world

Usando Goland, tendremos abierto el proyecto “golang-examples”, hacemos clic derecho sobre el fichero y en el menu contextual hacemos clic en Run

Cuando escribimos un programa en Go, debe tener un package main. Este paquete debe tener también una func main dentro de él.

Testeando

Lo mejor es separar en funciones nuestro código para no estar imprimiendo por pantalla en cada test. Digamos que imprimir por pantalla sería un “side-effects” no deseado. Por lo que vamos a dejar nuestro código así:

En hello.go

package main

import "fmt"

func main() {
    fmt.Println(HelloWorld())
}

func HelloWorld() string {
    return "Hello, world"
}

Hemos aprendido como se declaran funciones, que en las funciones debemos especificar el tipo de salida. Sabemos imprimir por pantalla y vamos a tener en cuenta un regla para el futuro Las funciones públicas deben comenzar por mayúscula

Ahora que está separado vamos a por el test.

Creando el test (requisitos)

Lo primero es crear un fichero que termine en _test.go y como regla generar lo mejor es crearlo justo al lado del que tenemos. Así que creamos el fichero hello_test.go

Los test deben empezar por la palabra Test.

Además la funciones de test solo tienen un parámetro de entrada t *testing.T

Este sería nuestro fichero de test:

En hello_test.go

package main

import "testing"

func TestHelloWorld(t *testing.T) {
    expected := "Hello, world"
    got := HelloWorld()

    if expected != got {
        t.Errorf("got '%s' expected '%s'", got, expected)
    }
}

Con esto ya tenemos el test listo. Para ejecutarlo:

$ go test
PASS
ok      github.com/jeslopcru/golang-examples/hello      0.006s

Para ejecutarlo en Goland, tan solo es necesario clic derecho sobre el fichero de test y luego run.

Hemos aprendido a ejecutar los tests, también hemos aprendido a Declarar variables con name := value. Del mismo modo hemos usado t.Errorf para indicar cuando hay un error en el test.

Todo funcionando, ¿empezamos con TDD?

Tenemos todo funcionando, un pequeño fichero para test, código de producción, sabemos cómo ejecutarlo. Ahora vamos a dar un pequeño paso más. Vamos a hacer que este HelloWorld no saludes si le pasamos un nombre, sino que siga diciendo Hello world.

Modificando el test

Empezaremos modificando el test que tenemos por algo como esto:

func TestHelloWorld(t *testing.T) {
    expected := "Hello, Jesús"
    got := HelloWorld("Jesús")

    if expected != got {
        t.Errorf("got '%s' expected '%s'", got, expected)
    }
}

Ya Goland nos indica que tenemos un error y cuando intentamos ejecutar los tests pasa lo siguiente:

# github.com/jeslopcru/golang-examples/hello
./hello_test.go:7:19: too many arguments in call to HelloWorld
    have (string)
    want ()

Modificando código

Así que lo que vamos a hacer es modificar la función HelloWorld para que pase el Test.

En hello.go

func HelloWorld(name string) string {
    return "Hello, " + name
}

Hemos aprendido a como utilizar parámetros de entrada en una función. también sabemos concatenar

Refactor

Es un paso un poco rápido, pero vemos que "Hello, " siempre se repite. Así que vamos a extraerlo como constante

En hello.go

const HELLO = "Hello, "
func HelloWorld(name string) string {
    return HELLO + name
}

Hemos aprendido como se crean constantes, con la palabra reservada CONST

Volviendo al inicio

Vamos a intentar tener la funcionalidad inicial, es decir: Si ponemos nuestro nombre queremos que no salude, pero en cambio, si el parámetro de entrada está vacío queremos que diga ‘Hello, World’

Podemos crear un nuevo test, o crear distintos casos dentro del mismo. Como hemos venido a aprender allá vamos:

En hello_test.go 

func TestHelloWorld(t *testing.T) {

    t.Run("when say hello with a name then response is hello name", func(t *testing.T) {
        expected := "Hello, Jesús"
        got := HelloWorld("Jesús")

        if expected != got {
            t.Errorf("got '%s' expected '%s'", got, expected)
        }
    })

    t.Run("when say hello with empty name then response is say hello world", func(t *testing.T) {
        expected := "Hello, World"
        got := HelloWorld("")

        if got != expected {
            t.Errorf("got '%s' expected '%s'", got, expected)
        }
    })
}

Hemos aprendido a crear casos de test dentro de un test para que estos sean más legibles.

Al ejecutarlo tendremos un error como éste:

=== RUN   TestHelloWorld/when_say_hello_with_empty_name_then_response_is_say_hello_world
    --- FAIL: TestHelloWorld/when_say_hello_with_empty_name_then_response_is_say_hello_world (0.00s)
        hello_test.go:21: got 'Hello, ' expected 'Hello, World'

Así que para arreglarlo modificamos el código de producción:

En hello.go

func HelloWorld(name string) string {
    if name == "" {
        name = "World"
    }
    return HELLO + name
}

Hemos aprendido a hacer comparaciones de una cadena vacía.

¿Eso es todo? La verdad es que no. El paso de refactoring en TDD no es solo para el código de producción sino tambien para los test. Vamos a eliminar el código repetido de los test.

En hello_test.go nos quedará algo así:

package main

import "testing"

func TestHelloWorld(t *testing.T) {

    t.Run("when say hello with a name then response is hello name", func(t *testing.T) {
        expected := "Hello, Jesús"
        got := HelloWorld("Jesús")

        assertIsCorrectMessage(got, expected, t)
    })

    t.Run("when say hello with empty name then response is say hello world", func(t *testing.T) {
        expected := "Hello, World"
        got := HelloWorld("")

        assertIsCorrectMessage(got, expected, t)
    })
}
func assertIsCorrectMessage(got string, expected string, t *testing.T) {
    if got != expected {
        t.Errorf("got '%s' expected '%s'", got, expected)
    }
}

Hemos aprendido a extraer un método

Podemos hacerlo un poco más funcional así:

En hello_test.go

package main

import "testing"

func TestHelloWorld(t *testing.T) {

    assertIsCorrectMessage := func(got string, expected string, t *testing.T) {
        t.Helper()
        if got != expected {
            t.Errorf("got '%s' expected '%s'", got, expected)
        }
    }
    t.Run("when say hello with a name then response is hello name", func(t *testing.T) {
        expected := "Hello, Jesús"
        got := HelloWorld("Jesús")

        assertIsCorrectMessage(got, expected, t)
    })

    t.Run("when say hello with empty name then response is say hello world", func(t *testing.T) {
        expected := "Hello, World"
        got := HelloWorld("")

        assertIsCorrectMessage(got, expected, t)
    })
}

Con t.Helper() le estamos diciendo a Go que esta función es de ayuda, así cuando ocurra un fallo en los tests nos dirá donde fue.

Conclusiones

Aprender Go con un buen IDE, es mucho más sencillo, ya que el Golang intenta ayudarte. Por ejemplo si ponemos parentesis en if name == "" { nos dirá que son innecesarios. También el formateo de código es importante y por ultimo lo que hacemos con TDD es focalizarnos en un problema cada vez.

Como recordatorio de TDD:

  • Escribir un test
  • Ejecutar para saber que de verdad falla
  • Escribir el código para que pase
  • Refactorizar

No refactorizar en rojo y baby steps

¿Y si añadimos idiomas para saludar? Algo así: HelloWorld("Yisus", "spanish") => Hola, Yisus o en frances HelloWorld("Jesús", "french")=> Salut, Jesús

Bola Extra

Muchas veces necesitamos Internet para buscar ejemplo, pero otras veces con la documentación es suficiente. usando

godoc -http :8000

Tenemos toda la documentación de Go lista para usar desde un navegador en localhost:8000/pkg

Anuncios

Comenta la entrada

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

w

Conectando a %s