Как проверить эквивалентность карт в Голанге?


86

У меня есть тестовый пример, управляемый таблицей, вроде этого:

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

Я мог бы проверить, одинаковы ли длины, и написать цикл, который проверяет, одинаковы ли все пары ключ-значение. Но потом мне придется снова написать эту проверку, когда я захочу использовать ее для другого типа карты (скажем map[string]string).

В итоге я преобразовал карты в строки и сравнил строки:

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

Это предполагает, что строковые представления эквивалентных карт одинаковы, что кажется правдой в этом случае (если ключи одинаковы, они хешируют одно и то же значение, поэтому их порядок будет одинаковым). Есть лучший способ сделать это? Каков идиоматический способ сравнения двух карт в табличных тестах?


4
Err, no: порядок итерации карты не является предсказуемым : «Порядок итерации по картам не указан и не гарантируется, что он будет одинаковым от одной итерации к следующей. ...» .
zzzz

2
Кроме того, для карт определенных размеров Go намеренно изменяет порядок в случайном порядке. Настоятельно рекомендуется не зависеть от этого порядка.
Джереми Уолл,

Попытка сравнить карту - это недостаток дизайна вашей программы.
Inanc Gumus 05

4
Обратите внимание, что в версии go 1.12 (февраль 2019 г.) карты теперь распечатываются в порядке сортировки по ключам для облегчения тестирования . Смотрите мой ответ ниже
VonC,

Ответы:


165

Библиотека Go уже вас покрыла. Сделай это:

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

Если вы посмотрите на исходный код для случая reflect.DeepEqual' Map, вы увидите, что сначала он проверяет, равны ли обе карты нулю, затем проверяет, имеют ли они одинаковую длину, прежде чем, наконец, проверить, есть ли у них одинаковый набор (ключ, значение) пары.

Поскольку reflect.DeepEqualпринимает тип интерфейса, он будет работать на любой действующей карте ( map[string]bool, map[struct{}]interface{}и т. Д.). Обратите внимание, что он также будет работать со значениями, не относящимися к карте, поэтому будьте осторожны, потому что на самом деле вы передаете ему две карты. Если вы передадите ему два целых числа, он с радостью скажет вам, равны ли они.


Круто, это именно то, что я искал. Я думаю, поскольку jnml говорил, что это не так эффективно, но кого это волнует в тестовом примере.
andras

Да, если вы когда-нибудь захотите этого для производственного приложения, я бы определенно пошел с пользовательской функцией, если это возможно, но это определенно поможет, если производительность не является проблемой.
joshlf

1
@andras Вы также должны проверить gocheck . Так же просто, как c.Assert(m1, DeepEquals, m2). Что хорошо в этом, так это то, что он прерывает тест и сообщает вам, что вы получили и чего ожидали в результате.
Люк,

8
Стоит отметить, что DeepEqual также требует, чтобы ПОРЯДОК срезов был одинаковым .
Xeoncross


13

Каков идиоматический способ сравнения двух карт в табличных тестах?

У вас есть проект, go-test/deepчтобы помочь.

Но: это должно быть проще с Go 1.12 (февраль 2019 г.) изначально : см. Примечания к выпуску .

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

fmt

Карты теперь распечатываются в порядке сортировки по ключам для облегчения тестирования .

Правила заказа:

  • Если применимо, ноль сравнивает низкий
  • целые числа, числа с плавающей запятой и строки упорядочиваются по <
  • NaN сравнивает числа с плавающей запятой, не являющиеся NaN
  • boolсравнивает falseраньшеtrue
  • Комплекс сравнивает реальное, а затем воображаемое
  • Указатели сравниваются по машинному адресу
  • Значения каналов сравниваются по машинному адресу
  • Структуры сравнивают каждое поле по очереди
  • Массивы по очереди сравнивают каждый элемент
  • Значения интерфейса сравниваются сначала по reflect.Typeописанию конкретного типа, а затем по конкретному значению, как описано в предыдущих правилах.

При печати карт нерефлексивные ключевые значения, такие как NaN, ранее отображались как <nil>. Начиная с этого выпуска печатаются правильные значения.

Источники:

CL добавляет: ( CL означает «Список изменений» )

Для этого мы добавляем в корень пакетinternal/fmtsort , который реализует общий механизм сортировки ключей карты независимо от их типа.

Это немного беспорядочно и, вероятно, медленно, но форматированная печать карт никогда не была быстрой и уже всегда основана на отражении.

Новый пакет является внутренним, потому что мы действительно не хотим, чтобы все использовали его для сортировки. Он медленный, не общий и подходит только для подмножества типов, которые могут быть ключами карты.

Также используйте пакет, в text/templateкотором уже была более слабая версия этого механизма.

Вы можете видеть, что используется в src/fmt/print.go#printValue(): case reflect.Map:


Извините за мое невежество, я новичок в Go, но как именно это новое fmtповедение помогает проверить эквивалентность карт? Вы предлагаете сравнить строковые представления вместо использования DeepEqual?
sschuberth

@sschuberth DeepEqualвсе еще хорош. (или, скорееcmp.Equal ) вариант использования более проиллюстрирован в twitter.com/mikesample/status/1084223662167711744 , например, журналы сравнения, как указано в исходном выпуске: github.com/golang/go/issues/21095 . Это означает: в зависимости от характера вашего теста может помочь надежный diff.
VonC

fmt.Sprint(map1) == fmt.Sprint(map2)для tl; dr
425nesp

@ 425nesp Спасибо. Я соответствующим образом отредактировал ответ.
VonC

11

Вот что я бы сделал (непроверенный код):

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
}

Хорошо, но у меня есть другой тестовый пример, в котором я хочу сравнить экземпляры map[string]float64. eqработает только для map[string]intкарт. Должен ли я реализовывать версию eqфункции каждый раз, когда я хочу сравнить экземпляры нового типа карты?
andras

@andras: 11 SLOC. Я бы "копипаст" специализировался на этом быстрее, чем нужно, чтобы спросить об этом. Хотя многие другие использовали бы «отразить», чтобы сделать то же самое, но с гораздо худшей производительностью.
zzzz

1
разве это не предполагает, что карты будут в том же порядке? Какой вариант
nathj07,

3
@ nathj07 Нет, потому что мы перебираем только через a.
Торстен Бронгер 02

5

Отказ от ответственности : не map[string]intсвязано, но связано с тестированием эквивалентности карт в Go, что является заголовком вопроса

Если у вас есть карта типа указателя (например map[*string]int), то вы ничего не хотите использовать reflect.DeepEqual , потому что он возвращает ложь.

Наконец, если ключ является типом, содержащим неэкспортированный указатель, например time.Time, то рефлексия.DeepEqual на такой карте также может возвращать false .


2

Используйте метод "Diff" на github.com/google/go-cmp/cmp :

Код:

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

Выход:

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",
+       },
    },
  }

1

Самый простой способ:

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

Пример:

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

1

Вместо этого используйте cmp ( https://github.com/google/go-cmp ):

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

Неудачный тест

Он все равно терпит неудачу, если "порядок" карты в ожидаемом выводе не соответствует тому, что возвращает ваша функция. Тем не менее, cmpон все же может указать, в чем заключается несоответствие.

Для справки я нашел этот твит:

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

«Использование в тестах Reflections.DeepEqual - зачастую плохая идея, поэтому мы открываем исходный код http://github.com/google/go-cmp » - Джо Цай


-5

Один из вариантов - исправить rng:

rand.Reader = mathRand.New(mathRand.NewSource(0xDEADBEEF))

Простите, а как ваш ответ связан с этим вопросом?
Дима Кожевин

@DimaKozhevin golang внутренне использует rng для смешивания порядка записей на карте. Если вы исправите rng, вы получите предсказуемый порядок для целей тестирования.
Grozz

@Grozz Это правда? Зачем!? Я не обязательно оспариваю, что это могло быть (я понятия не имею), я просто не понимаю, почему это так.
msanford

Я не работаю над Golang, поэтому не могу объяснить их доводы, но это подтвержденное поведение, по крайней мере, с версии 1.9. Однако я видел некоторые объяснения вроде «мы хотим обеспечить, чтобы вы не зависели от упорядочивания на картах, потому что не должны».
Grozz 08
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.