Cómo hacer Testing en go, parte 2

in #programming7 years ago (edited)

Lo prometido es deuda, en la primera parte de este post explique cómo hacer testing en go con un ejemplo sencillo con la idea de tener las bases, entender el concepto para luego poder dar un salto y pasar a un ejemplo más sofisticado y cercano a la realidad.

La idea es analizar una posible solución de como hacer un testing a una función que me regresa un arreglo de objetos json, qué es lo que comúnmente se usa hoy en día en los servicios REST.

Voy a partir de la siguiente suposición:

Tengo una librería con una función, esa función recibe un parámetro de tipo string y lo qué regresa son 2 valores, un raw string es decir un objeto json y un nil de tipo error.
Es función se llama SearchTerm y se ve así:

func SearchTerm(term string) (string, error) {

    if len(term) == 0 {
        return "", errors.New("No string supplied")
    }

    searchResult :=`[{"ciudad": "Puebla","colonia": "Villa de Cortes","cp": "72764","delegacion": "San Pedro Cholula","location": {"lat": 19.575,"lon": -97.8916}}]`

    return searchResult, nil

}

En la variable searchResult hice una asignación implícita := a un raw string, donde ese raw string es un arreglo de un objeto json (nota los corchetes cuadrados al inicio y al final). Ese arreglo bien pudo haber tenido n elementos, pero le puse uno solo para hacer el ejemplo más sencillo y fácil de entender, pero igual debe de funcionar con más elementos no importa.

Ahora el código completo lo voy a poner en un archivo qué se llame rest.go y va a quedar de la siguiente manera:

package main

import (
    "errors"
    "fmt"
)

func main() {

    result, _ := SearchTerm("cortes villa")
    fmt.Printf("%v \n", result)
}

func SearchTerm(term string) (string, error) {

    if len(term) == 0 {
        return "", errors.New("No string supplied")
    }

    searchResult :=`[{"ciudad": "Puebla","colonia": "Villa de Cortes","cp": "72764","delegacion": "San Pedro Cholula","location": {"lat": 19.575,"lon": -97.8916}}]`

    return searchResult, nil

}

Al ejecutar el programa con:

go run rest.go

Obtengo la siguiente salida:

[{"ciudad": "Puebla","colonia": "Villa de Cortes","cp": "72764","delegacion": "San Pedro Cholula","location": {"lat": 19.5
75,"lon": -97.8916}}] 

Esta sería la simulación de la respuesta del servicio rest.
Esa respuesta bien pudo haber venido de un servidor MySQL, MongoDB, ElasticSearch, etc, etc.
Ahora que tengo la salida de la función como un arreglo de objetos json lo que sigue es hacer el archivo rest_test.go.
Basandome en la explicación qué di en el primer post de testing, la primera versión del código quedaría como sigue:

package main

import (
    "testing"
)

func TestSearchTerm(t *testing.T) {

    input := "cortes villa"

    expected :=
        `[
        {
            "ciudad": "Puebla",
            "colonia": "Villa de Cortes",
            "cp": "72764",
            "delegacion": "San Pedro Cholula",
            "location": {
                "lat": 19.575,
                "lon": -97.8916
            }
        }
    ]`

    result, _ := SearchTerm(input)

    if result != expected {
        t.Errorf("\nResult: \n\n %f  \n\nExpected: \n\n %f \n\n", result, expected)
    }

}

Y hasta este punto nada nuevo, solamente mencionar los 4 ingredientes qué necesito para hacer un testing:

  • La variable de entrada con su respectivo valor:
input := "cortes villa"

  • La variable result qué guarda el resultado de la función que quiero probar SearchTerm:
result, _ := SearchTerm(input)

  • La variable expected, qué guarda el resultado que espero que sea igual al de la función SearchTerm.
expected :=
        `[
        {
            "ciudad": "Puebla",
            "colonia": "Villa de Cortes",
            "cp": "72764",
            "delegacion": "San Pedro Cholula",
            "location": {
                "lat": 19.575,
                "lon": -97.8916
            }
        }
    ]`

  • Y la condición donde se evalúa si pasa o no la prueba
    if result != expected {
        t.Errorf("\nResult: \n\n %f  \n\nExpected: \n\n %f \n\n", result, expected)
    }

Al ejecutar ejecutar la prueba con el comando:

go test -v

Obtengo la siguiente salida:

=== RUN   TestSearchTerm
--- FAIL: TestSearchTerm (0.00s)
       elasticsearch_test.go:35:  
               Result:  

                %!f(string=[{"ciudad": "Puebla","colonia": "Villa de Cortes","cp": "72764","delegacion": "San Pedro Cholu
la","location": {"lat": 19.575,"lon": -97.8916}}])   

               Expected:  

                %!f(string=[
                       {
                           "ciudad": "Puebla",
                           "colonia": "Villa de Cortes",
                           "cp": "72764",
                           "delegacion": "San Pedro Cholula",
                           "location": {
                               "lat": 19.575,
                               "lon": -97.8916
                           }
                       }
                   ])  

FAIL
exit status 1
FAIL    github.com/hectorgool/test      0.001s

Y aquí empiezan los problemas.

Si hago una inspección de los valores de las propiedades de los objetos json, el del resultado y el esperado, me doy cuenta de qué son los mismos, pero al hacer la comparación de las cadenas go me dice que son diferentes.

La primera solución que se me viene a la mente es “ponle los mismos espacios y saltos de línea a los 2 objetos json”, para que empaten las cadenas y pase la prueba.

Invertí varias minutos poniendo y quitando espacios en blanco y saltos de línea a ambos objetos json para que empataran y creeme que nunca puede hacer que hicieran match aunque aparentemente lucían igual, razón por la cual habría que buscar otra solución.

Lo segundo que se me ocurre es empezar a buscar en foros o preguntar si es que existe una función que reciba una objeto json y me regrese el mismo objeto con un formato estándar o uniforme de tal manera que se lo pueda aplicar a los 2 objetos y con eso pueda hacer el match de las 2 cadenas.

Después de darme un tiempo y despejarme, ya sabes como suele suceder en segundos llega la idea con la solución.

Y por qué no pasar esos objetos json a structs, cuando los paso a structs quedan con un formato estándar y así ya los puedo comparar!

Entonces lo primero que hago es definir las estructuras con el formato que quiero:

type Document struct {
    Ciudad     string `json:"ciudad"`
    Colonia    string `json:"colonia"`
    Cp         string `json:"cp"`
    Delegacion string `json:"delegacion"`
    Location   `json:"location"`
}

type Location struct {
    Lat float32 `json:"lat"`
    Lon float32 `json:"lon"`
}

Luego usa la función Unmarshal que pertenece al paquete json para decodificar esos objetos json, o dicho de otra manera para pasar de un json a una struct de go.

Esa función queda así:

func jsonTostruct(in string) []Document {
    var out []Document
    err := json.Unmarshal([]byte(in), &out)
    if err != nil {
        fmt.Println(err)
    }
    return out
}

Haciendo los respectivos cambios en la condición me lleva a este nuevo problema:

    struct_result := jsonTostruct(result)
    struct_expected := jsonTostruct(expected)

    if struct_result != struct_expected {
        t.Errorf("\nResult: \n\n %f  \n\nExpected: \n\n %f \n\n", struct_result, struct_expected)
    }

Con la anterior sintaxis no puedo comparar 2 slices de esa forma, por qué me mando un error que muestra lo siguiente:

invalid operation: struct_result != struct_expected (slice can only be compared to nil)
FAIL    github.com/hectorgool/test [build failed]

Esto es por qué lo qué estoy comparado son arreglos de structs.

Finalmente buscando en StackOverflow me lleva a la solución que es usar la función DeepEqual de paquete reflect qué me sirve para cotejar arreglos que tengan exactamente los mismos elementos qué es lo que necesito para este test.

Por lo cual la condición final de la prueba queda así:

    struct_result := jsonTostruct(result)
    struct_expected := jsonTostruct(expected)

    if ! reflect.DeepEqual(struct_result, struct_expected) {
        t.Errorf("\nResult: \n\n %f  \n\nExpected: \n\n %f \n\n", struct_result, struct_expected)
    }

Con esta versión si vuelvo a ejecuar la prueba:

go test -v

Pasa la prueba :)

=== RUN   TestSearchTerm
--- PASS: TestSearchTerm (0.00s)
PASS
ok     /test      0.001s

A continuación de las versiones finales del codigo con los respectivos ajustes:

rest.go:

package main

import (
    "errors"
    "fmt"
)

type Document struct {
    Ciudad     string `json:"ciudad"`
    Colonia    string `json:"colonia"`
    Cp         string `json:"cp"`
    Delegacion string `json:"delegacion"`
    Location   `json:"location"`
}

type Location struct {
    Lat float32 `json:"lat"`
    Lon float32 `json:"lon"`
}

func main() {

    result, _ := SearchTerm("cortes villa")
    fmt.Printf("%v \n", result)
}

func SearchTerm(term string) (string, error) {

    if len(term) == 0 {
        return "", errors.New("No string supplied")
    }

    searchResult :=`[{"ciudad": "Puebla","colonia": "Villa de Cortes","cp": "72764","delegacion": "San Pedro Cholula","location": {"lat": 19.575,"lon": -97.8916}}]`

    return searchResult, nil

}

y rest_test.go

package main

import (
    "encoding/json"
    "fmt"
    "testing"
    "reflect"
)

func TestSearchTerm(t *testing.T) {

    input := "cortes villa"

    expected :=
    `[
        {
            "ciudad": "Puebla",
            "colonia": "Villa de Cortes",
            "cp": "72764",
            "delegacion": "San Pedro Cholula",
            "location": {
                "lat": 19.575,
                "lon": -97.8916
            }
        }
    ]`

    result, _ := SearchTerm(input)

    struct_result := jsonTostruct(result)
    struct_expected := jsonTostruct(expected)

    if ! reflect.DeepEqual(struct_result, struct_expected) {
        t.Errorf("\nResult: \n\n %f  \n\nExpected: \n\n %f \n\n", struct_result, struct_expected)
    }

}


func jsonTostruct(in string) []Document {
    var out []Document
    err := json.Unmarshal([]byte(in), &out)
    if err != nil {
        fmt.Println(err)
    }
    return out
}


Cualquier duda pregunta o comentario que sirva para mejorar es siempre bienvenido

Sort:  

Thanks elsanto I translate your post to English, please keep your Go posts coming. I'm learning Go and just posted about JetBrains latest development platform called Gogland.

https://steemit.com/go/@strapasynthon/gogland-the-latest-go-language-development-platform-from-jetbrains

Thank you!

Congratulations @elsanto! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of upvotes received

Click on any badge to view your own Board of Honor on SteemitBoard.
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

By upvoting this notification, you can help all Steemit users. Learn how here!