¿Cómo probar la equivalencia de mapas en Golang?

86

Tengo un caso de prueba basado en tablas como este:

func CountWords(s string) map[string]int

func TestCountWords(t *testing.T) {
  var tests = []struct {
    input string
    want map[string]int
  }{
    {"foo", map[string]int{"foo":1}},
    {"foo bar foo", map[string]int{"foo":2,"bar":1}},
  }
  for i, c := range tests {
    got := CountWords(c.input)
    // TODO test whether c.want == got
  }
}

Podría comprobar si las longitudes son las mismas y escribir un bucle que compruebe si todos los pares clave-valor son iguales. Pero luego tengo que escribir este cheque nuevamente cuando quiera usarlo para otro tipo de mapa (digamos map[string]string).

Lo que terminé haciendo es convertir los mapas en cadenas y comparar las cadenas:

func checkAsStrings(a,b interface{}) bool {
  return fmt.Sprintf("%v", a) != fmt.Sprintf("%v", b) 
}

//...
if checkAsStrings(got, c.want) {
  t.Errorf("Case #%v: Wanted: %v, got: %v", i, c.want, got)
}

Esto supone que las representaciones de cadenas de mapas equivalentes son las mismas, lo que parece ser cierto en este caso (si las claves son las mismas, entonces obtienen el mismo valor, por lo que sus órdenes serán las mismas). ¿Hay una mejor manera de hacer esto? ¿Cuál es la forma idiomática de comparar dos mapas en pruebas basadas en tablas?

andras
fuente
4
Err, no: No se garantiza que el orden de iteración de un mapa sea predecible : "El orden de iteración de los mapas no se especifica y no se garantiza que sea el mismo de una iteración a la siguiente ..." .
zzzz
2
Además, para mapas de ciertos tamaños, Go aleatorizará intencionalmente el orden. Es muy recomendable no depender de ese orden.
Jeremy Wall
Tratar de comparar un mapa es un defecto de diseño en su programa.
Inanc Gumus
4
Tenga en cuenta que con go 1.12 (febrero de 2019), los mapas ahora se imprimen en orden de clave para facilitar las pruebas . Vea mi respuesta a continuación
VonC

Respuestas:

165

La biblioteca Go ya lo tiene cubierto. Hacer esto:

import "reflect"
// m1 and m2 are the maps we want to compare
eq := reflect.DeepEqual(m1, m2)
if eq {
    fmt.Println("They're equal.")
} else {
    fmt.Println("They're unequal.")
}

Si observa el código fuente del caso de reflect.DeepEqual' Map, verá que primero verifica si ambos mapas son nulos, luego verifica si tienen la misma longitud antes de verificar finalmente si tienen el mismo conjunto de (clave, valor) pares.

Debido a que reflect.DeepEqualtoma un tipo de interfaz, funcionará en cualquier mapa válido ( map[string]bool, map[struct{}]interface{}, etc.). Tenga en cuenta que también funcionará con valores que no sean mapas, así que tenga cuidado de que lo que le está pasando sean realmente dos mapas. Si le pasa dos enteros, felizmente le dirá si son iguales.

Joshlf
fuente
Impresionante, eso es exactamente lo que estaba buscando. Supongo que, como decía jnml, no es tan eficaz, pero a quién le importa en un caso de prueba.
andras
Sí, si alguna vez quiere esto para una aplicación de producción, definitivamente iría con una función escrita a medida si es posible, pero esto definitivamente funciona si el rendimiento no es una preocupación.
joshlf
1
@andras También deberías revisar gocheck . Tan simple como c.Assert(m1, DeepEquals, m2). Lo bueno de esto es que aborta la prueba y le dice lo que obtuvo y lo que esperaba en la salida.
Lucas
8
Vale la pena señalar que DeepEqual también requiere que el ORDEN de los cortes sea igual .
Xeoncross
13

¿Cuál es la forma idiomática de comparar dos mapas en pruebas basadas en tablas?

Tienes el proyecto go-test/deeppara ayudarte.

Pero: esto debería ser más fácil con Go 1.12 (febrero de 2019) de forma nativa : consulte las notas de la versión .

fmt.Sprint(map1) == fmt.Sprint(map2)

fmt

Los mapas ahora se imprimen en orden de claves para facilitar las pruebas .

Las reglas para ordenar son:

  • Cuando corresponde, cero compara bajo
  • ints, flotadores y cadenas ordenados por <
  • NaN compara menos de flotadores que no son NaN
  • boolcompara falseantestrue
  • Complejo compara real, luego imaginario
  • Los punteros se comparan por dirección de máquina
  • Los valores de canal se comparan por dirección de máquina
  • Las estructuras comparan cada campo por turno
  • Las matrices comparan cada elemento a su vez
  • Los valores de interfaz se comparan primero reflect.Typedescribiendo el tipo concreto y luego por valor concreto como se describe en las reglas anteriores.

Al imprimir mapas, los valores clave no reflexivos como NaN se mostraban anteriormente como <nil>. A partir de esta versión, se imprimen los valores correctos.

Fuentes:

La CL agrega: ( CL significa "Lista de cambios" )

Para hacer esto, agregamos un paquete en la raíz,internal/fmtsort que implementa un mecanismo general para ordenar las claves del mapa independientemente de su tipo.

Esto es un poco complicado y probablemente lento, pero la impresión formateada de mapas nunca ha sido rápida y siempre se basa en la reflexión.

El nuevo paquete es interno porque realmente no queremos que todos los utilicen para ordenar las cosas. Es lento, no general, y solo es adecuado para el subconjunto de tipos que pueden ser claves de mapa.

También use el paquete en text/template, que ya tenía una versión más débil de este mecanismo.

Puedes ver que se usa en src/fmt/print.go#printValue(): case reflect.Map:

VonC
fuente
Perdón por mi ignorancia, soy nuevo en Go, pero ¿cómo fmtayuda exactamente este nuevo comportamiento a probar la equivalencia de los mapas? ¿Sugiere comparar las representaciones de cadenas en lugar de utilizarlas DeepEqual?
sschuberth
@sschuberth DeepEqualsigue siendo bueno. (o más biencmp.Equal ) El caso de uso está más ilustrado en twitter.com/mikesample/status/1084223662167711744 , como registros diferentes como se indica en el número original: github.com/golang/go/issues/21095 . Significado: dependiendo de la naturaleza de su prueba, una diferencia confiable puede ayudar.
VonC
fmt.Sprint(map1) == fmt.Sprint(map2)para el tl; dr
425nesp
@ 425nesp Gracias. He editado la respuesta en consecuencia.
VonC
11

Esto es lo que haría (código no probado):

func eq(a, b map[string]int) bool {
        if len(a) != len(b) {
                return false
        }

        for k, v := range a {
                if w, ok := b[k]; !ok || v != w {
                        return false
                }
        }

        return true
}
zzzz
fuente
Está bien, pero tengo otro caso de prueba en el que quiero comparar instancias de map[string]float64. eqsolo funciona para map[string]intmapas. ¿Debo implementar una versión de la eqfunción cada vez que quiera comparar instancias de un nuevo tipo de mapa?
andras
@andras: 11 SLOC. Me especializaría en "copiar y pegar" en menos tiempo del que se necesita para preguntar sobre esto. Sin embargo, muchos otros usarían "reflejar" para hacer lo mismo, pero eso tiene un rendimiento mucho peor.
zzzz
1
¿No espera eso que los mapas estén en el mismo orden? Lo que no garantiza ver "Orden de iteración" en blog.golang.org/go-maps-in-action
nathj07
3
@ nathj07 No, porque solo iteramos a.
Torsten Bronger
5

Descargo de responsabilidad : no map[string]intrelacionado pero relacionado con probar la equivalencia de mapas en Go, que es el título de la pregunta

Si usted tiene un mapa de un tipo de puntero (como map[*string]int), entonces qué no querer usar reflect.DeepEqual porque va a devolver false.

Finalmente, si la clave es un tipo que contiene un puntero no exportado, como time.Time, reflect.DeepEqual en dicho mapa también puede devolver falso .

Carl
fuente
2

Utilice el método "Diff" de github.com/google/go-cmp/cmp :

Código:

// Let got be the hypothetical value obtained from some logic under test
// and want be the expected golden data.
got, want := MakeGatewayInfo()

if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff)
}

Salida:

MakeGatewayInfo() mismatch (-want +got):
  cmp_test.Gateway{
    SSID:      "CoffeeShopWiFi",
-   IPAddress: s"192.168.0.2",
+   IPAddress: s"192.168.0.1",
    NetMask:   net.IPMask{0xff, 0xff, 0x00, 0x00},
    Clients: []cmp_test.Client{
        ... // 2 identical elements
        {Hostname: "macchiato", IPAddress: s"192.168.0.153", LastSeen: s"2009-11-10 23:39:43 +0000 UTC"},
        {Hostname: "espresso", IPAddress: s"192.168.0.121"},
        {
            Hostname:  "latte",
-           IPAddress: s"192.168.0.221",
+           IPAddress: s"192.168.0.219",
            LastSeen:  s"2009-11-10 23:00:23 +0000 UTC",
        },
+       {
+           Hostname:  "americano",
+           IPAddress: s"192.168.0.188",
+           LastSeen:  s"2009-11-10 23:03:05 +0000 UTC",
+       },
    },
  }
Jonas Felber
fuente
1

Manera más sencilla:

    assert.InDeltaMapValues(t, got, want, 0.0, "Word count wrong. Got %v, want %v", got, want)

Ejemplo:

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

func TestCountWords(t *testing.T) {
    got := CountWords("hola hola que tal")

    want := map[string]int{
        "hola": 2,
        "que": 1,
        "tal": 1,
    }

    assert.InDeltaMapValues(t, got, want, 0.0, "Word count wrong. Got %v, want %v", got, want)
}
miqrc
fuente
1

Utilice cmp ( https://github.com/google/go-cmp ) en su lugar:

if !cmp.Equal(src, expectedSearchSource) {
    t.Errorf("Wrong object received, got=%s", cmp.Diff(expectedSearchSource, src))
}

Prueba fallida

Todavía falla cuando el "orden" del mapa en su salida esperada no es lo que devuelve su función. Sin embargo, cmptodavía puede señalar dónde está la inconsistencia.

Como referencia, encontré este tweet:

https://twitter.com/francesc/status/885630175668346880?lang=es

"usar reflect.DeepEqual en las pruebas es a menudo una mala idea, por eso abrimos http://github.com/google/go-cmp de código abierto " - Joe Tsai

ericson.cepeda
fuente
-5

Una de las opciones es arreglar rng:

rand.Reader = mathRand.New(mathRand.NewSource(0xDEADBEEF))
Grozz
fuente
Disculpe, pero ¿cómo se relaciona su respuesta con esta pregunta?
Dima Kozhevin
@DimaKozhevin golang usa internamente rng para mezclar el orden de las entradas en un mapa. Si corrige el rng, obtendrá un orden predecible con fines de prueba.
Grozz
@Grozz ¿Lo hace? ¿¡Por qué!? No estoy necesariamente discutiendo que podría (no tengo idea), simplemente no veo por qué lo haría.
msanford
No trabajo en Golang, así que no puedo explicar su razonamiento, pero ese es un comportamiento confirmado al menos a partir de la v1.9. Sin embargo, vi una explicación en la línea de "queremos hacer cumplir que no puede depender de ordenar en mapas, porque no debería".
Grozz