Como testar a equivalência de mapas em Golang?

86

Eu tenho um caso de teste baseado em tabela 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
  }
}

Eu poderia verificar se os comprimentos são iguais e escrever um loop que verifica se todos os pares de valores-chave são iguais. Mas então eu tenho que escrever essa verificação novamente quando quiser usá-la para outro tipo de mapa (digamos map[string]string).

O que acabei fazendo é converter os mapas em strings e comparar as strings:

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)
}

Isso pressupõe que as representações de string de mapas equivalentes são as mesmas, o que parece ser verdade neste caso (se as chaves forem as mesmas, então eles hash para o mesmo valor, portanto, suas ordens serão as mesmas). Existe uma maneira melhor de fazer isso? Qual é a maneira idiomática de comparar dois mapas em testes baseados em tabelas?

andras
fonte
4
Err, não: A ordem de iteração de um mapa não é garantida como previsível : "A ordem de iteração nos mapas não é especificada e não é garantida a mesma de uma iteração para a próxima. ..." .
zzzz
2
Além disso, para mapas de certos tamanhos, Go irá aleatorizar a ordem intencionalmente. É altamente recomendável não depender dessa ordem.
Jeremy Wall
Tentar comparar um mapa é uma falha de design em seu programa.
Inanc Gumus de
4
Observe que com o go 1.12 (fevereiro de 2019), os mapas agora são impressos em ordem classificada por chave para facilitar o teste . Veja minha resposta abaixo
VonC

Respostas:

165

A biblioteca Go já está ajudando você. Faça isso:

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.")
}

Se você olhar o código-fonte para reflect.DeepEqualo Mapcaso de, verá que ele primeiro verifica se ambos os mapas são nulos, depois verifica se eles têm o mesmo comprimento antes de finalmente verificar se eles têm o mesmo conjunto de (chave, valor) pares.

Por ter reflect.DeepEqualum tipo de interface, funcionará em qualquer mapa válido ( map[string]bool, map[struct{}]interface{}, etc). Observe que ele também funcionará em valores que não sejam do mapa, portanto, tome cuidado para que o que você está passando sejam realmente dois mapas. Se você passar dois inteiros, ficará feliz em saber se eles são iguais.

Joshlf
fonte
Incrível, era exatamente isso que eu estava procurando. Eu acho que como jnml estava dizendo não é tão bom desempenho, mas quem se importa em um caso de teste.
andras
Sim, se você quiser isso para um aplicativo de produção, eu definitivamente escolheria uma função personalizada, se possível, mas isso definitivamente resolve se o desempenho não for uma preocupação.
joshlf
1
@andras Você também deve verificar o gocheck . Tão simples quanto c.Assert(m1, DeepEquals, m2). O que é bom nisso é que ele aborta o teste e informa o que você obteve e o que esperava na saída.
Lucas
8
É importante notar que DeepEqual também exige que a ORDER das fatias seja igual .
Xeoncross
13

Qual é a maneira idiomática de comparar dois mapas em testes baseados em tabelas?

Você tem o projeto go-test/deeppara ajudar.

Mas: isso deve ser mais fácil com Go 1.12 (fevereiro de 2019) nativamente : consulte as notas de lançamento .

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

fmt

Os mapas agora são impressos em ordem classificada para facilitar o teste .

As regras de pedido são:

  • Quando aplicável, nil se compara a baixo
  • ints, floats e strings ordenados por <
  • NaN compara menos do que flutuadores não NaN
  • boolcompara falseantestrue
  • Complexo compara real e imaginário
  • Os ponteiros comparam por endereço de máquina
  • Os valores do canal são comparados por endereço da máquina
  • As estruturas comparam cada campo por vez
  • Arrays comparam cada elemento por vez
  • Os valores da interface são comparados primeiro, reflect.Typedescrevendo o tipo concreto e, em seguida, pelo valor concreto, conforme descrito nas regras anteriores.

Ao imprimir mapas, valores-chave não reflexivos como NaN eram exibidos anteriormente como <nil>. A partir desta versão, os valores corretos são impressos.

Fontes:

O CL adiciona: ( CL significa "Lista de Mudanças" )

Para fazer isso, adicionamos um pacote na raiz,internal/fmtsort que implementa um mecanismo geral para classificar as chaves do mapa, independentemente de seu tipo.

Isso é um pouco confuso e provavelmente lento, mas a impressão formatada de mapas nunca foi rápida e já é sempre baseada em reflexão.

O novo pacote é interno porque realmente não queremos que todo mundo use isso para classificar as coisas. É lento, não geral e adequado apenas para o subconjunto de tipos que podem ser chaves de mapa.

Use também o pacote em text/template, que já tinha uma versão mais fraca desse mecanismo.

Você pode ver que usado em src/fmt/print.go#printValue(): case reflect.Map:

VonC
fonte
Desculpe pela minha ignorância, sou novo no Go, mas como exatamente esse novo fmtcomportamento ajuda a testar a equivalência de mapas? Você está sugerindo comparar as representações de string em vez de usar DeepEqual?
sschuberth
@sschuberth DeepEqualainda é bom. (ou melhorcmp.Equal ) O caso de uso é mais ilustrado em twitter.com/mikesample/status/1084223662167711744 , como a comparação de logs conforme declarado no problema original: github.com/golang/go/issues/21095 . Significado: dependendo da natureza do seu teste, um diff confiável pode ajudar.
VonC
fmt.Sprint(map1) == fmt.Sprint(map2)para o tl; dr
425 nsp de
@ 425nesp Obrigado. Eu editei a resposta de acordo.
VonC
11

Isso é o que eu faria (código não testado):

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
fonte
OK, mas tenho outro caso de teste onde desejo comparar instâncias de map[string]float64. eqsó funciona para map[string]intmapas. Devo implementar uma versão da eqfunção sempre que quiser comparar instâncias de um novo tipo de mapa?
andras
@andras: 11 SLOCs. Eu "copiaria e colaria", especializaria em menos tempo do que leva para perguntar sobre isso. Porém, muitos outros usariam "refletir" para fazer o mesmo, mas isso tem um desempenho muito pior.
zzzz
1
isso não espera que os mapas estejam na mesma ordem? O que não garante ver "Ordem de iteração" em blog.golang.org/go-maps-in-action
nathj07
3
@ nathj07 Não, porque iteramos apenas através de a.
Torsten Bronger
5

Isenção de responsabilidade : não map[string]intrelacionado, mas relacionado ao teste de equivalência de mapas em Go, que é o título da pergunta

Se você tem um mapa de um tipo de ponteiro (como map[*string]int), então você não não quer usar reflect.DeepEqual porque ele vai retornar falso.

Finalmente, se a chave for um tipo que contém um ponteiro não exportado, como time.Time, então, reflect.DeepEqual em tal mapa também pode retornar false .

Carl
fonte
2

Use o 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)
}

Resultado:

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
fonte
1

Maneira mais simples:

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

Exemplo:

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
fonte
1

Em vez disso, use cmp ( https://github.com/google/go-cmp ):

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

Teste reprovado

Ele ainda falha quando a "ordem" do mapa em sua saída esperada não é o que sua função retorna. Porém, cmpainda é capaz de apontar onde está a inconsistência.

Para referência, encontrei este tweet:

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

"usar reflect.DeepEqual em testes costuma ser uma má ideia, é por isso que abrimos o código-fonte http://github.com/google/go-cmp " - Joe Tsai

ericson.cepeda
fonte
-5

Uma das opções é corrigir o rng:

rand.Reader = mathRand.New(mathRand.NewSource(0xDEADBEEF))
Grozz
fonte
Com licença, mas como sua resposta está relacionada a esta pergunta?
Dima Kozhevin de
@DimaKozhevin golang usa internamente rng para misturar a ordem das entradas em um mapa. Se você corrigir o rng, obterá uma ordem previsível para fins de teste.
Grozz
@Grozz, sim? Por quê!? Não estou necessariamente contestando que poderia (não tenho ideia), mas não vejo por que faria.
msanford
Não trabalho em Golang, então não posso explicar o raciocínio deles, mas esse é um comportamento confirmado pelo menos a partir da v1.9. No entanto, vi uma explicação do tipo "queremos garantir que você não pode depender da ordenação de mapas, porque você não deveria".
Grozz