Closed9

Gomega で Kubernetes オブジェクトの Assertion

zoetrozoetro

Gomega による Kubernetes オブジェクトの Assertion 方法を比較してみる。

zoetrozoetro

テスト用のオブジェクトを作成する関数を用意しておく。

package controllers

import (
	"k8s.io/utils/pointer"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func createDeployment(image string) *appsv1.Deployment {
	return &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "default",
			Name:      "nginx-deployment",
			Labels: map[string]string{
				"app": "nginx",
			},
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: pointer.Int32(3),
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"app": "nginx",
				},
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"app": "nginx",
					},
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "nginx",
							Image: image,
							Ports: []corev1.ContainerPort{
								{
									ContainerPort: 80,
								},
							},
						},
					},
				},
			},
		},
	}
}
zoetrozoetro

まずは単純に gomega.Equal を利用した方法。
わざとテストを失敗させてみる。

package controllers

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Equal", func() {
	It("should equal", func() {
		dep1 := createDeployment("nginx:latest")
		dep2 := createDeployment("nginx:1.14.2")

		Expect(dep1).Should(Equal(dep2))
	})
})

失敗メッセージは以下のようになり、非常に分かりにくい。

  Expected
      <*v1.Deployment | 0xc000b12000>: {
          TypeMeta: {Kind: "", APIVersion: ""},
          ObjectMeta: {
              Name: "nginx-deployment",
              GenerateName: "",
              Namespace: "default",
              SelfLink: "",
              UID: "",
              ResourceVersion: "",
              Generation: 0,
              CreationTimestamp: {
                  Time: 0001-01-01T00:00:00Z,
              },
              DeletionTimestamp: nil,
              DeletionGracePeriodSeconds: nil,
              Labels: {"app": "nginx"},
              Annotations: nil,
              OwnerReferences: nil,
              Finalizers: nil,
              ZZZ_DeprecatedClusterName: "",
              ManagedFields: nil,
          },
          Spec: {
              Replicas: 3,
              Selector: {
                  MatchLabels: {"app": "nginx"},
                  MatchExpressions: nil,
              },
              Template: {
                  ObjectMeta: {
                      Name: "",
                      GenerateName: "",
                      Namespace: "",
                      SelfLink: "",
                      UID: "",
                      ResourceVersion: "",
                      Generation: 0,
                      CreationTimestamp: {
                          Time: 0001-01-01T00:00:00Z,
                      },
                      DeletionTimestamp: nil,
                      DeletionGracePeriodSeconds: nil,
                      Labels: {"app": "nginx"},
                      Annotations: nil,
                      OwnerReferences: nil,
                      Finalizers: nil,
                      ZZZ_DeprecatedClusterName: "",
                      ManagedFields: nil,
                  },
                  Spec: {
                      Volumes: nil,
                      InitContainers: nil,
                      Containers: [
                          {
                              Name: "nginx",
                              Image: "nginx:latest",
                              Command: nil,
                              Args: nil,
                              WorkingDir: "",
                              Ports: [
                                  {Name: "", HostPort: 0, ContainerPort: 80, Protocol: "", HostIP: ""},
                              ],
                              EnvFrom: nil,
                              Env: nil,
                              Resources: {Limits: nil, Requests: nil},
                              VolumeMounts: nil,
                              VolumeDevices: nil,
                              LivenessProbe: nil,
                              ReadinessProbe: nil,
                              StartupProbe: nil,
                              Lifecycle: nil,
                              TerminationMessagePath: "",
                              TerminationMessagePolicy: "",
                              ImagePullPolicy: "",
                              SecurityContext: nil,
                              Stdin: false,
                              StdinOnce: false,
                              TTY: false,
                          },
                      ],
                      EphemeralContainers: nil,
                      RestartPolicy: "",
                      TerminationGracePeriodSeconds: nil,
                      ActiveDeadlineSeconds: nil,
                      DNSPolicy: "",
                      NodeSelector: nil,
                      ServiceAccountName: "",
                      DeprecatedServiceAccount: "",
                      AutomountServiceAccountToken: nil,
                      NodeName: "",
                      HostNetwork: false,
                      HostPID: false,
                      HostIPC: false,
                      ShareProcessNamespace: nil,
                      SecurityContext: nil,
                      ImagePullSecrets: nil,
                      Hostname: "",
                      Subdomain: "",
                      Affinity: nil,
                      SchedulerName: "",
                      Tolerations: nil,
                      HostAliases: nil,
                      PriorityClassName: "",
                      Priority: nil,
                      DNSC...

  Gomega truncated this representation as it exceeds 'format.MaxLength'.
  Consider having the object provide a custom 'GomegaStringer' representation
  or adjust the parameters in Gomega's 'format' package.

  Learn more here: https://onsi.github.io/gomega/#adjusting-output

  to equal
      <*v1.Deployment | 0xc000b12480>: {
          TypeMeta: {Kind: "", APIVersion: ""},
          ObjectMeta: {
              Name: "nginx-deployment",
              GenerateName: "",
              Namespace: "default",
              SelfLink: "",
              UID: "",
              ResourceVersion: "",
              Generation: 0,
              CreationTimestamp: {
                  Time: 0001-01-01T00:00:00Z,
              },
              DeletionTimestamp: nil,
              DeletionGracePeriodSeconds: nil,
              Labels: {"app": "nginx"},
              Annotations: nil,
              OwnerReferences: nil,
              Finalizers: nil,
              ZZZ_DeprecatedClusterName: "",
              ManagedFields: nil,
          },
          Spec: {
              Replicas: 3,
              Selector: {
                  MatchLabels: {"app": "nginx"},
                  MatchExpressions: nil,
              },
              Template: {
                  ObjectMeta: {
                      Name: "",
                      GenerateName: "",
                      Namespace: "",
                      SelfLink: "",
                      UID: "",
                      ResourceVersion: "",
                      Generation: 0,
                      CreationTimestamp: {
                          Time: 0001-01-01T00:00:00Z,
                      },
                      DeletionTimestamp: nil,
                      DeletionGracePeriodSeconds: nil,
                      Labels: {"app": "nginx"},
                      Annotations: nil,
                      OwnerReferences: nil,
                      Finalizers: nil,
                      ZZZ_DeprecatedClusterName: "",
                      ManagedFields: nil,
                  },
                  Spec: {
                      Volumes: nil,
                      InitContainers: nil,
                      Containers: [
                          {
                              Name: "nginx",
                              Image: "nginx:1.14.2",
                              Command: nil,
                              Args: nil,
                              WorkingDir: "",
                              Ports: [
                                  {Name: "", HostPort: 0, ContainerPort: 80, Protocol: "", HostIP: ""},
                              ],
                              EnvFrom: nil,
                              Env: nil,
                              Resources: {Limits: nil, Requests: nil},
                              VolumeMounts: nil,
                              VolumeDevices: nil,
                              LivenessProbe: nil,
                              ReadinessProbe: nil,
                              StartupProbe: nil,
                              Lifecycle: nil,
                              TerminationMessagePath: "",
                              TerminationMessagePolicy: "",
                              ImagePullPolicy: "",
                              SecurityContext: nil,
                              Stdin: false,
                              StdinOnce: false,
                              TTY: false,
                          },
                      ],
                      EphemeralContainers: nil,
                      RestartPolicy: "",
                      TerminationGracePeriodSeconds: nil,
                      ActiveDeadlineSeconds: nil,
                      DNSPolicy: "",
                      NodeSelector: nil,
                      ServiceAccountName: "",
                      DeprecatedServiceAccount: "",
                      AutomountServiceAccountToken: nil,
                      NodeName: "",
                      HostNetwork: false,
                      HostPID: false,
                      HostIPC: false,
                      ShareProcessNamespace: nil,
                      SecurityContext: nil,
                      ImagePullSecrets: nil,
                      Hostname: "",
                      Subdomain: "",
                      Affinity: nil,
                      SchedulerName: "",
                      Tolerations: nil,
                      HostAliases: nil,
                      PriorityClassName: "",
                      Priority: nil,
                      DNSC...

  Gomega truncated this representation as it exceeds 'format.MaxLength'.
  Consider having the object provide a custom 'GomegaStringer' representation
  or adjust the parameters in Gomega's 'format' package.

  Learn more here: https://onsi.github.io/gomega/#adjusting-output
zoetrozoetro

gomega.Equal は内部的に reflect.DeepEqual を利用しているが、これは Kubernetes のオブジェクト比較に向いていないケースがある。(オブジェクトが resource.Quantitylabels.Selector などを含んでいる場合)

そこで、equality.Semantic.DeepEqual を利用したカスタムマッチャーを用意する。
なお、差分の表示にはgo-cmpを利用する。

package controllers

import (
	"fmt"

	"github.com/google/go-cmp/cmp"
	"github.com/onsi/gomega/types"
	"k8s.io/apimachinery/pkg/api/equality"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/labels"
)

func SemanticEqual(expected interface{}) types.GomegaMatcher {
	return &semanticMatcher{
		expected: expected,
		compare:  equality.Semantic.DeepEqual,
	}
}

func SemanticDerivative(expected interface{}) types.GomegaMatcher {
	return &semanticMatcher{
		expected: expected,
		compare:  equality.Semantic.DeepDerivative,
	}
}

type semanticMatcher struct {
	expected interface{}
	compare  func(a1, a2 interface{}) bool
}

func (matcher *semanticMatcher) Match(actual interface{}) (bool, error) {
	return matcher.compare(matcher.expected, actual), nil
}

var diffOptions = []cmp.Option{
	cmp.Comparer(func(x, y resource.Quantity) bool {
		return x.Cmp(y) == 0
	}),
	cmp.Comparer(func(a, b metav1.MicroTime) bool {
		return a.UTC() == b.UTC()
	}),
	cmp.Comparer(func(a, b metav1.Time) bool {
		return a.UTC() == b.UTC()
	}),
	cmp.Comparer(func(a, b labels.Selector) bool {
		return a.String() == b.String()
	}),
	cmp.Comparer(func(a, b fields.Selector) bool {
		return a.String() == b.String()
	}),
}

func (matcher *semanticMatcher) FailureMessage(actual interface{}) (message string) {
	diff := cmp.Diff(actual, matcher.expected, diffOptions...)
	return fmt.Sprintf("diff: \n%s", diff)
}

func (matcher *semanticMatcher) NegatedFailureMessage(actual interface{}) (message string) {
	diff := cmp.Diff(actual, matcher.expected, diffOptions...)
	return fmt.Sprintf("diff: \n%s", diff)
}
zoetrozoetro

上記のカスタムマッチャーを使って、Assertion をわざと失敗させてみる。

package controllers

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("SemanticEqual", func() {
	It("should equal", func() {
		dep1 := createDeployment("nginx:latest")
		dep2 := createDeployment("nginx:1.14.2")

		Expect(dep1).Should(SemanticEqual(dep2))
	})
})

失敗メッセージは先ほどより大分わかりやすい。

  diff:
    &v1.Deployment{
        TypeMeta:   {},
        ObjectMeta: {Name: "nginx-deployment", Namespace: "default", Labels: {"app": "nginx"}},
        Spec: v1.DeploymentSpec{
                Replicas: &3,
                Selector: &{MatchLabels: {"app": "nginx"}},
                Template: v1.PodTemplateSpec{
                        ObjectMeta: {Labels: {"app": "nginx"}},
                        Spec: v1.PodSpec{
                                Volumes:        nil,
                                InitContainers: nil,
                                Containers: []v1.Container{
                                        {
                                                Name:    "nginx",
  -                                             Image:   "nginx:latest",
  +                                             Image:   "nginx:1.14.2",
                                                Command: nil,
                                                Args:    nil,
                                                ... // 18 identical fields
                                        },
                                },
                                EphemeralContainers: nil,
                                RestartPolicy:       "",
                                ... // 31 identical fields
                        },
                },
                Strategy:        {},
                MinReadySeconds: 0,
                ... // 3 identical fields
        },
        Status: {},
    }
zoetrozoetro

しかし、実際のテストでは equality.Semantic.DeepEqual が使えないことのほうが多い。
そこで、Gomega の提供している Matcher を利用して愚直に書いてみる。

package controllers

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("GomegaMatcher", func() {
	It("should equal", func() {
		dep1 := createDeployment("nginx:latest")

		Expect(dep1.Namespace).Should(Equal("default"))
		Expect(dep1.Name).Should(Equal("nginx-deployment"))
		Expect(dep1.Labels).Should(HaveKeyWithValue("app", "nginx"))
		Expect(dep1.Spec.Replicas).Should(HaveValue(BeNumerically("==", 3)))
		Expect(dep1.Spec.Selector).ShouldNot(BeNil())
		Expect(dep1.Spec.Selector.MatchLabels).Should(HaveKeyWithValue("app", "nginx"))
		Expect(dep1.Spec.Template.Labels).Should(HaveKeyWithValue("app", "nginx"))
		Expect(dep1.Spec.Template.Spec.Containers).ShouldNot(BeEmpty())
		Expect(dep1.Spec.Template.Spec.Containers[0].Name).Should(Equal("nginx"))
		Expect(dep1.Spec.Template.Spec.Containers[0].Image).Should(Equal("nginx:1.14.2"))
		Expect(dep1.Spec.Template.Spec.Containers[0].Ports).ShouldNot(BeEmpty())
		Expect(dep1.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort).Should(BeNumerically("==", 80))
	})
})

失敗メッセージはシンプル。

  Expected
      <string>: nginx:latest
  to equal
      <string>: nginx:1.14.2

HaveField や ConsistOf を使ってもう少しいい感じにできないかやってみる。

package controllers

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = FDescribe("ConsistOf", func() {
	It("should equal", func() {
		dep1 := createDeployment("nginx:latest")

		Expect(dep1).Should(HaveField("Namespace", Equal("default")))
		Expect(dep1).Should(HaveField("Name", Equal("nginx-deployment")))
		Expect(dep1).Should(HaveField("Labels", HaveKeyWithValue("app", "nginx")))
		Expect(dep1).Should(HaveField("Spec.Replicas", HaveValue(BeNumerically("==", 3))))
		Expect(dep1).ShouldNot(HaveField("Spec.Selector", BeNil()))
		Expect(dep1).Should(HaveField("Spec.Selector.MatchLabels", HaveKeyWithValue("app", "nginx")))
		Expect(dep1).Should(HaveField("Spec.Template.Labels", HaveKeyWithValue("app", "nginx")))
		Expect(dep1).ShouldNot(HaveField("Spec.Template.Spec.Containers", BeEmpty()))
		Expect(dep1.Spec.Template.Spec.Containers).Should(ConsistOf(SatisfyAll(
			HaveField("Name", Equal("nginx")),
			HaveField("Image", Equal("nginx:1.14.2")),
			HaveField("Ports", Not(BeEmpty())),
			HaveField("Ports", ConsistOf(HaveField("ContainerPort", BeNumerically("==", 80)))),
		)))
	})
})

エラーメッセージがかなり分かりにくい。

  Expected
      <[]v1.Container | len:1, cap:1>: [
          {
              Name: "nginx",
              Image: "nginx:latest",
              Command: nil,
              Args: nil,
              WorkingDir: "",
              Ports: [
                  {Name: "", HostPort: 0, ContainerPort: 80, Protocol: "", HostIP: ""},
              ],
              EnvFrom: nil,
              Env: nil,
              Resources: {Limits: nil, Requests: nil},
              VolumeMounts: nil,
              VolumeDevices: nil,
              LivenessProbe: nil,
              ReadinessProbe: nil,
              StartupProbe: nil,
              Lifecycle: nil,
              TerminationMessagePath: "",
              TerminationMessagePolicy: "",
              ImagePullPolicy: "",
              SecurityContext: nil,
              Stdin: false,
              StdinOnce: false,
              TTY: false,
          },
      ]
  to consist of
      <[]*matchers.AndMatcher | len:1, cap:1>: [
          {
              Matchers: [
                  <*matchers.HaveFieldMatcher | 0xc000ab7f40>{
                      Field: "Name",
                      Expected: <*matchers.EqualMatcher | 0xc00045fec0>{
                          Expected: <string>"nginx",
                      },
                      extractedField: <string>"nginx",
                      expectedMatcher: <*matchers.EqualMatcher | 0xc00045fec0>{
                          Expected: <string>"nginx",
                      },
                  },
                  <*matchers.HaveFieldMatcher | 0xc000ab7f80>{
                      Field: "Image",
                      Expected: <*matchers.EqualMatcher | 0xc00045fee0>{
                          Expected: <string>"nginx:1.14.2",
                      },
                      extractedField: <string>"nginx:latest",
                      expectedMatcher: <*matchers.EqualMatcher | 0xc00045fee0>{
                          Expected: <string>"nginx:1.14.2",
                      },
                  },
                  <*matchers.HaveFieldMatcher | 0xc000ab7fc0>{
                      Field: "Ports",
                      Expected: <*matchers.NotMatcher | 0xc00045ff00>{
                          Matcher: <*matchers.BeEmptyMatcher | 0x256e138>{},
                      },
                      extractedField: nil,
                      expectedMatcher: nil,
                  },
                  <*matchers.HaveFieldMatcher | 0xc0006d4040>{
                      Field: "Ports",
                      Expected: <*matchers.ConsistOfMatcher | 0xc00003cb90>{
                          Elements: [
                              <*matchers.HaveFieldMatcher | 0xc0006d4000>{
                                  Field: "ContainerPort",
                                  Expected: <*matchers.BeNumericallyMatcher | 0xc000891e30>{Comparator: "==", CompareTo: [<int>80]},
                                  extractedField: nil,
                                  expectedMatcher: nil,
                              },
                          ],
                          missingElements: nil,
                          extraElements: nil,
                      },
                      extractedField: nil,
                      expectedMatcher: nil,
                  },
              ],
              firstFailedMatcher: <*matchers.HaveFieldMatcher | 0xc000ab7f80>{
                  Field: "Image",
                  Expected: <*matchers.EqualMatcher | 0xc00045fee0>{
                      Expected: <string>"nginx:1.14.2",
                  },
                  extractedField: <string>"nginx:latest",
                  expectedMatcher: <*matchers.EqualMatcher | 0xc00045fee0>{
                      Expected: <string>"nginx:1.14.2",
                  },
              },
          },
      ]
  the missing elements were
      <[]*matchers.AndMatcher | len:1, cap:1>: [
          {
              Matchers: [
                  <*matchers.HaveFieldMatcher | 0xc000ab7f40>{
                      Field: "Name",
                      Expected: <*matchers.EqualMatcher | 0xc00045fec0>{
                          Expected: <string>"nginx",
                      },
                      extractedField: <string>"nginx",
                      expectedMatcher: <*matchers.EqualMatcher | 0xc00045fec0>{
                          Expected: <string>"nginx",
                      },
                  },
                  <*matchers.HaveFieldMatcher | 0xc000ab7f80>{
                      Field: "Image",
                      Expected: <*matchers.EqualMatcher | 0xc00045fee0>{
                          Expected: <string>"nginx:1.14.2",
                      },
                      extractedField: <string>"nginx:latest",
                      expectedMatcher: <*matchers.EqualMatcher | 0xc00045fee0>{
                          Expected: <string>"nginx:1.14.2",
                      },
                  },
                  <*matchers.HaveFieldMatcher | 0xc000ab7fc0>{
                      Field: "Ports",
                      Expected: <*matchers.NotMatcher | 0xc00045ff00>{
                          Matcher: <*matchers.BeEmptyMatcher | 0x256e138>{},
                      },
                      extractedField: nil,
                      expectedMatcher: nil,
                  },
                  <*matchers.HaveFieldMatcher | 0xc0006d4040>{
                      Field: "Ports",
                      Expected: <*matchers.ConsistOfMatcher | 0xc00003cb90>{
                          Elements: [
                              <*matchers.HaveFieldMatcher | 0xc0006d4000>{
                                  Field: "ContainerPort",
                                  Expected: <*matchers.BeNumericallyMatcher | 0xc000891e30>{Comparator: "==", CompareTo: [<int>80]},
                                  extractedField: nil,
                                  expectedMatcher: nil,
                              },
                          ],
                          missingElements: nil,
                          extraElements: nil,
                      },
                      extractedField: nil,
                      expectedMatcher: nil,
                  },
              ],
              firstFailedMatcher: <*matchers.HaveFieldMatcher | 0xc000ab7f80>{
                  Field: "Image",
                  Expected: <*matchers.EqualMatcher | 0xc00045fee0>{
                      Expected: <string>"nginx:1.14.2",
                  },
                  extractedField: <string>"nginx:latest",
                  expectedMatcher: <*matchers.EqualMatcher | 0xc00045fee0>{
                      Expected: <string>"nginx:1.14.2",
                  },
              },
          },
      ]
  the extra elements were
      <[]v1.Container | len:1, cap:1>: [
          {
              Name: "nginx",
              Image: "nginx:latest",
              Command: nil,
              Args: nil,
              WorkingDir: "",
              Ports: [
                  {Name: "", HostPort: 0, ContainerPort: 80, Protocol: "", HostIP: ""},
              ],
              EnvFrom: nil,
              Env: nil,
              Resources: {Limits: nil, Requests: nil},
              VolumeMounts: nil,
              VolumeDevices: nil,
              LivenessProbe: nil,
              ReadinessProbe: nil,
              StartupProbe: nil,
              Lifecycle: nil,
              TerminationMessagePath: "",
              TerminationMessagePolicy: "",
              ImagePullPolicy: "",
              SecurityContext: nil,
              Stdin: false,
              StdinOnce: false,
              TTY: false,
          },
      ]
zoetrozoetro

Gomega には複雑な構造体をアサーションするための gstruct パッケージが用意されているので、これを利用してみる。

package controllers

import (
	"strconv"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	. "github.com/onsi/gomega/gstruct"
	corev1 "k8s.io/api/core/v1"
)

var _ = Describe("gstruct", func() {
	It("should equal", func() {
		dep1 := createDeployment("nginx:latest")

		Expect(dep1).Should(PointTo(MatchFields(IgnoreExtras, Fields{
			"ObjectMeta": MatchFields(IgnoreExtras, Fields{
				"Namespace": Equal("default"),
				"Name":      Equal("nginx-deployment"),
				"Labels":    MatchAllKeys(Keys{"app": Equal("nginx")}),
			}),
			"Spec": MatchFields(IgnoreExtras, Fields{
				"Replicas": PointTo(BeNumerically("==", 3)),
				"Selector": PointTo(MatchFields(IgnoreExtras, Fields{
					"MatchLabels": MatchAllKeys(Keys{"app": Equal("nginx")}),
				})),
				"Template": MatchFields(IgnoreExtras, Fields{
					"ObjectMeta": MatchFields(IgnoreExtras, Fields{
						"Labels": MatchAllKeys(Keys{"app": Equal("nginx")}),
					}),
					"Spec": MatchFields(IgnoreExtras, Fields{
						"Containers": MatchAllElements(containerIdentity, Elements{
							"nginx": MatchFields(IgnoreExtras, Fields{
								"Image": Equal("nginx:1.14.2"),
								"Ports": MatchAllElements(portIdentity, Elements{
									"80": HaveField("ContainerPort", BeNumerically("==", 80)),
								}),
							}),
						},
						),
					}),
				}),
			}),
		})))
	})
})

func containerIdentity(element interface{}) string {
	container, ok := element.(corev1.Container)
	if !ok {
		return ""
	}
	return container.Name
}

func portIdentity(element interface{}) string {
	port, ok := element.(corev1.ContainerPort)
	if !ok {
		return ""
	}
	return strconv.FormatInt(int64(port.ContainerPort), 10)
}

エラーメッセージは非常にわかりやすい。
どのフィールドが間違っているのかが分かるし、複数箇所の間違いも一度に表示してくれる。

  Expected
      <string>: Deployment
  to match fields: {
  .Spec.Template.Spec.Containers[nginx].Image:
        Expected
            <string>: nginx:latest
        to equal
            <string>: nginx:1.14.2
  }
zoetrozoetro

kmatch というライブラリを見つけたのでこれを使ってみる。
テストはかなりシンプルに書くことができる。

package controllers

import (
	. "github.com/kralicky/kmatch"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("kmatch", func() {
	It("should equal", func() {
		dep1 := createDeployment("nginx:latest")

		Expect(dep1).Should(SatisfyAll(
			HaveNamespace("default"),
			HaveName("nginx-deployment"),
			HaveLabels("app", "nginx"),
			HaveReplicaCount(3),
			HaveMatchingContainer(SatisfyAll(
				HaveName("nginx"),
				HaveImage("nginx:1.14.2"),
				HavePorts(80),
			)),
		))
	})
})

しかし、失敗したときのメッセージは以下のようになり、どこが間違っているのか分からない。

  expected nginx-deployment to have a matching container
zoetrozoetro

結論としては、比較的単純な比較ですむ場合は equality.Semantic.DeepEqual, equality.Semantic.DeepDerivative のカスタムマッチャーを利用し、そうでない場合は gstruct を利用するのがよさそう。

このスクラップは2022/08/14にクローズされました