Як перевірити еквівалентність карт у Голангу?


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
Помилка, ні: Порядок ітерації карти не гарантується передбачуваним : "Порядок ітерацій на картах не вказаний і не гарантується однаковим від однієї ітерації до наступної ..." .
zzzz

2
Крім того, для карт певних розмірів Go буде навмисно рандомізувати замовлення. Дуже бажано не залежати від цього замовлення.
Джеремі Уолл

Спроба порівняти карту - це недолік дизайну вашої програми.
Inanc Gumus

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«S 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

Карти тепер друкуються в порядку сортування ключів для полегшення тестування .

Правила впорядкування:

  • За необхідності, нуль порівнює низький
  • ints, floats та strings упорядкувати за <
  • 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 . Значення: залежно від характеру тесту може допомогти надійний розбіжність.
VonC

fmt.Sprint(map1) == fmt.Sprint(map2)для tl; dr
425несп

@ 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
хіба це не очікує, що карти будуть в однаковому порядку? Що не гарантує, перегляньте "Порядок ітерацій" на blog.golang.org/go-maps-in-action
nathj07

3
@ nathj07 Ні, тому що ми повторюємо лише через a.
Торстен Бронгер,

5

Застереження : не пов'язане зmap[string]int пов'язане з тестуванням еквівалентності карт у Go, але це пов'язано з ним, що є заголовком питання

Якщо у вас є карта типу покажчика (наприклад map[*string]int), то ви нічого НЕ хочете використовувати reflect.DeepEqual , тому що він повертає брехня.

Нарешті, якщо ключ - це тип, що містить не експортований вказівник, наприклад time.Time, тоді Reflect.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=uk

"використання Reflect.DeepEqual у тестах часто є поганою ідеєю, тому ми відкриваємо http://github.com/google/go-cmp " - Джо Цай


-5

Одним із варіантів є виправлення rng:

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

Вибачте, але як ваша відповідь пов'язана з цим питанням?
Діма Кожевін

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

@Grozz Це робить? Чому !? Я не обов'язково заперечую, що це може (я не маю уявлення), я просто не розумію, чому це може.
Мсанфорд,

Я не працюю над Голангом, тому не можу пояснити їх аргументацію, але це підтверджена поведінка, принаймні станом на v1.9. Однак я побачив пояснення в думці "ми хочемо домогтися того, що ти не можеш залежати від замовлення на картах, тому що не повинен".
Grozz
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.