Golang에서지도의 동등성을 테스트하는 방법은 무엇입니까?


92

다음과 같은 테이블 기반 테스트 케이스가 있습니다.

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는 의도적으로 순서를 무작위로 지정합니다. 그 순서에 의존하지 않는 것이 좋습니다.
— Jeremy Wall

지도를 비교하는 것은 프로그램의 디자인 결함입니다.
— Inanc Gumus 2019

4
go 1.12 (2019 년 2 월)에서는 테스트를 쉽게하기 위해지도가 키 정렬 순서로 인쇄됩니다 . 아래 내 대답을
— VonC

답변:


174

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). 이것에 대해 좋은 점은 테스트를 중단하고 출력에서 ​​얻은 것과 예상 한 것을 알려준다는 것입니다.
— Luke

8
DeepEqual 은 슬라이스의 ORDER 도 동일 해야 한다는 점은 주목할 가치가 있습니다 .
— Xeoncross 2017


13

테이블 기반 테스트에서 두 맵을 비교하는 관용적 방법은 무엇입니까?

당신은 go-test/deep도울 프로젝트 가 있습니다.

하지만 기본적으로 Go 1.12 (2019 년 2 월)를 사용하면이 작업이 더 쉬워 질 것입니다 . 출시 노트를 참조하세요 .

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

fmt

이제 맵은 테스트를 쉽게하기 위해 키 정렬 순서로 인쇄됩니다 .

주문 규칙은 다음과 같습니다.

  • 적용 가능한 경우 nil은 낮음을 비교합니다.
  • 정수, 부동 소수점 및 문자열 순서 <
  • NaN은 비 NaN 수레보다 적은 수를 비교합니다.
  • boolfalse전에 비교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 diffing의 같은 로그 : 원래 문제에 명시된대로 github.com/golang/go/issues/21095을 . 의미 : 테스트의 특성에 따라 신뢰할 수있는 diff가 도움이 될 수 있습니다.
— VonC

fmt.Sprint(map1) == fmt.Sprint(map2)for the 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좋습니다. 하지만 .NET의 인스턴스를 비교하려는 다른 테스트 케이스가 있습니다. 지도 eq에서만 작동 map[string]int합니다. eq새로운 유형의지도 인스턴스를 비교하고 싶을 때마다 함수 버전을 구현해야합니까 ?
— andras 2013-08-13

@andras : 11 개의 SLOC. 나는 이것에 대해 물어 보는 것보다 더 짧은 시간에 그것을 "복사 붙여 넣기"전문화 할 것이다. 하지만 다른 많은 사람들이 "반사"를 사용하여 동일한 작업을 수행하지만 성능이 훨씬 더 떨어집니다.
— zzzz

1
지도의 순서가 같을 것으로 기대하지 않습니까? blog.golang.org/go-maps-in-action
— nathj07

3
@ nathj07 아니요, a.
— Torsten Bronger 2016

5

면책 조항 : map[string]int질문의 제목 인 Go에서지도의 동등성 테스트와 관련이 없지만 관련이 있음

당신이 포인터 타입의지도가있는 경우 (같은 map[*string]int), 당신은 할 수 없습니다 reflect.DeepEqual를 사용하려면 false를 반환 때문입니다.

마지막으로, 키가 time.Time과 같이 내 보내지 않은 포인터를 포함하는 유형 인 경우 이러한지도에서 reflect.DeepEqual 도 false를 반환 할 수 있습니다 .


3

github.com/google/go-cmp/cmp 의 "Diff"방법을 사용합니다 .

암호:

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

2

대신 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

"테스트에서 reflect.DeepEqual을 사용하는 것은 종종 나쁜 생각입니다. 그래서 우리는 http://github.com/google/go-cmp를 오픈 소스로 제공합니다. "-Joe Tsai


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

-5

옵션 중 하나는 rng를 수정하는 것입니다.

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

실례 합니다만이 질문과 귀하의 답변은 어떤 관련이 있습니까?
— Dima Kozhevin 2018

@DimaKozhevin golang은 내부적으로 rng를 사용하여 맵의 항목 순서를 혼합합니다. rng를 수정하면 테스트 목적으로 예측 가능한 순서를 얻을 수 있습니다.
— Grozz

@Grozz 그렇습니까? 왜!? 나는 그것이 그럴지도 모른다고 반드시 이의를 제기하지는 않습니다.
— msanford

나는 Golang에서 일하지 않기 때문에 그들의 추론을 설명 할 수는 없지만 적어도 v1.9에서 확인 된 행동입니다. 그러나 나는 "우리는 당신이지도에서 주문에 의존해서는 안되는 것을 강요하고 싶다"는 줄에 따라 몇 가지 설명을 보았다.
— Grozz
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.