Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go: Update tests to use cmp.Diff instead of reflect.DeepEqual #1244

Merged
merged 1 commit into from
May 8, 2022

Conversation

thempatel
Copy link
Contributor

What type of PR is this?

Uncomment one line below and remove others.

Other

What package or component does this PR mostly affect?

language/go

What does this PR do? Why is it needed?

reflect.DeepEqual is a poorer chose when choosing to compare test results. For example, DeepEqual will only compare pointer values using their memory address, rather than the underlying value in the type.

This is a pre-factor to make #1243 easier to review (per review feedback)

Which issues(s) does this PR fix?

Fixes #1099

Other notes for review

Copy link
Member

@achew22 achew22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely agree with the direction of this change, swapping reflect out for cmp, but I think we should be careful to bear in mind Go's Testing Style Guide on this matter (under the heading "Assert Libraries". The "idiomatic go" (I do not like that phrase) way of approaching these things would be to run the cmp.Diff inline with an if statement. Something like

if diff := cmp.Diff(want, got); diff != "" {
  t.Errorf("Path was unequal (-want, +got): %s", diff)
}

I would be very happy to merge a change that used the pattern suggested by the Go team with the cmp package instead of reflect.

)

func equals(t *testing.T, expected, actual interface{}, format string, args ...interface{}) {
allTypes := func(_ reflect.Type) bool { return true }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand why asserting on all types is the correct thing to do here? What fields do you care about that aren't exported? Are they a thing that actually should be a part of the public API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to make testing easier on unexported types, cmp.Diff will only compare exported fields by default which is pretty useless if you want to use it to validate your un-exported types in tests.

@thempatel
Copy link
Contributor Author

I completely agree with the direction of this change, swapping reflect out for cmp, but I think we should be careful to bear in mind Go's Testing Style Guide on this matter (under the heading "Assert Libraries". The "idiomatic go" (I do not like that phrase) way of approaching these things would be to run the cmp.Diff inline with an if statement. Something like

if diff := cmp.Diff(want, got); diff != "" {
  t.Errorf("Path was unequal (-want, +got): %s", diff)
}

I would be very happy to merge a change that used the pattern suggested by the Go team with the cmp package instead of reflect.

I am happy to make the suggested change! For dialogue purposes only: I'm not sure I personally agree with the guidance in that wiki. I've been writing go code professionally for a few years now (not that that's some indication that I've been doing it correctly these past years) and most opinions/idiomatic guidance that go suggests, I normally agree with.

but this either stops the test early (if assert calls t.Fatalf or panic)

I think in most cases, or at least the ones I've predominantly seen, Fatal/Fatalf is the correct choice. Tests are often written procedurally which means that continuing a test after the first error isn't going to test the correct behavior: it's the same reason you return an err the moment you find one, and not all at the end.

If the reason you'd want a test to continue after the first error is so that you can assert on the various values of a returned structure, or multiple values not in a structure, it's almost always better to just assert on the structure itself or put the disparate values in a structure, and use cmp.Diff to tell you want was different. Indeed, this is actually the same guidance in the above TestComments wiki:

If your function returns a struct, don't write test code that performs an individual comparison for each field of the struct.

Go has good support for printing structures

I don't see this as a win if the helpers you write, also just do this for you, like I've done here:

func equals(t *testing.T, expected, actual interface{}, format string, args ...interface{})

Assert libraries make it too easy to write imprecise tests and inevitably end up duplicating features already in the language, like expression evaluation, comparisons, sometimes even more.

I think the irony here is that go-cmp basically breaks the "duplicating features in the language ... like comparisons" and yet the guidance in the same doc is to actually use the library.

I'm all for consistency, and again I am going to make the changes you requested, but I think consistency, inconsistently applied has very little value

@thempatel
Copy link
Contributor Author

PTAL

@thempatel thempatel requested a review from achew22 April 29, 2022 12:33
@thempatel thempatel force-pushed the milan/go-test-updates branch 4 times, most recently from 49ee530 to dcdf49f Compare May 1, 2022 18:01
@achew22
Copy link
Member

achew22 commented May 1, 2022

Once again, I agree with the primary thrust of this effort, making it easier to debug but disagree with the implementation.

To briefly quote from the Assert Libraries documentation:

Assert libraries make it too easy to write imprecise tests

That is what I think is being violated by this change. It is not clear to me that cmpDiff brings clarity and precision to the tests at hand as evidenced by my comment:

Can you help me understand why asserting on all types is the correct thing to do here? What fields do you care about that aren't exported? Are they a thing that actually should be a part of the public API?

The logic of "why are these fields important to be compared" is completely elided when wrapping comparison in something like cmpDiff. The name doesn't describe that it is comparing all unexported fields and that is why it is unique, nor does it contain some indication that it is specifically targeted at these types in the type signature. It diffs a group of structs but does it in a non-standard way (I think there is a pretty strong case to be made that go-cmp is a part of the go standard library even if not officially yet).

Function composition is one way to solve problems, but there are many others. The go-cmp package is built around the notion of functional options and cmp.Diff takes a repeated Option, which I believe the authors intends to be the preferred mode of extending functionality. cmpDiff uses this functionality via Exporter to achieve that goal.

To quote from the go-cmp documentation on Exporters:

In many cases, a custom Comparer should be used instead that defines equality as a function of the public API of a type rather than the underlying unexported implementation.

For example, the reflect.Type documentation defines equality to be determined by the == operator on the interface (essentially performing a shallow pointer comparison) and most attempts to compare *regexp.Regexp types are interested in only checking that the regular expression strings are equal. Both of these are accomplished using Comparers:

Comparer(func(x, y reflect.Type) bool { return x == y })
Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() })

In other cases, the cmpopts.IgnoreUnexported option can be used to ignore all unexported fields on specified struct types.

My preference would be to heed the advice of go-cmp, and define reusable Equals functions on the various types you're interested in comparing. Something like this:

https://go.dev/play/p/02kN3A7712u

It advances the objective of making explicit that you're, in this case, only going to succeed if you're comparing foo objects, and it provides a useful method for people working in this library to compare equality of foo. In addition it affords the caller the continued ability to pass more options.

If you don't think this is the correct course, instead believing that the correct move is to export all the fields, I would much prefer you used the built in system for achieving such ends, AllowUnexported. The idea behind this is that you're selectively allowing individual types to be exported instead of just letting the comparison engine go to town.

https://go.dev/play/p/z7fpCgve66T

What do you think about these options?

@thempatel
Copy link
Contributor Author

thempatel commented May 2, 2022

It advances the objective of making explicit that you're, in this case, only going to succeed if you're comparing foo objects, and it provides a useful method for people working in this library to compare equality of foo. In addition it affords the caller the continued ability to pass more options.

Your example stops the diffing at the level for which you've defined the comparator. Example:

Diff (-want,+got):
  main.foo(
- 	{a: "a", b: "b", c: "c"},
+ 	{a: "1", b: "b", c: "c"},
  )

versus using AllowUnexported or the Exporter

Diff (-want,+got):
  main.foo{
- 	a: "a",
+ 	a: "1",
  	b: "b",
  	c: "c",
  }

Before I continue further, I'm curious to get your take on which output you prefer: 1 or 2? Ultimately I don't have to work in this repository in the day-to-day, so I am less motivated to advocate for QOL improvements that I will not be able to enjoy.

@achew22
Copy link
Member

achew22 commented May 4, 2022

#2 is probably what I'd do, it has less boilerplate/

@thempatel
Copy link
Contributor Author

@achew22 PTAL

@achew22
Copy link
Member

achew22 commented May 8, 2022

This is perfect! Thank you so much for your contribution!

@achew22 achew22 merged commit d740db9 into bazelbuild:master May 8, 2022
@thempatel thempatel deleted the milan/go-test-updates branch May 8, 2022 14:28
@thempatel
Copy link
Contributor Author

sweet, thanks for the review!

gcf-merge-on-green bot pushed a commit to googleapis/gapic-config-validator that referenced this pull request Jun 27, 2022
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [bazel_gazelle](https://togithub.com/bazelbuild/bazel-gazelle) | http_archive | minor | `v0.25.0` -> `v0.26.0` |

---

### Release Notes

<details>
<summary>bazelbuild/bazel-gazelle</summary>

### [`v0.26.0`](https://togithub.com/bazelbuild/bazel-gazelle/releases/tag/v0.26.0)

[Compare Source](https://togithub.com/bazelbuild/bazel-gazelle/compare/v0.25.0...v0.26.0)

#### What's Changed

-   fix(tests): fix gazelle_generation_test expected stderr update by [@&#8203;jbedard](https://togithub.com/jbedard) in [bazelbuild/bazel-gazelle#1220
-   Add an e2e test confirming no output on success by [@&#8203;achew22](https://togithub.com/achew22) in [bazelbuild/bazel-gazelle#1216
-   Update extend.md with a practical languages example by [@&#8203;Anthony-Bible](https://togithub.com/Anthony-Bible) in [bazelbuild/bazel-gazelle#1222
-   fix: Remove gazelle_binary import collision by [@&#8203;illicitonion](https://togithub.com/illicitonion) in [bazelbuild/bazel-gazelle#1226
-   Broaden label name regex by [@&#8203;illicitonion](https://togithub.com/illicitonion) in [bazelbuild/bazel-gazelle#1229
-   gazelle_generation_test: redact workspace path from output by [@&#8203;dr-dime](https://togithub.com/dr-dime) in [bazelbuild/bazel-gazelle#1231
-   Add -print0 to print names of rewritten files by [@&#8203;dr-dime](https://togithub.com/dr-dime) in [bazelbuild/bazel-gazelle#1213
-   Code Quality Improvements by [@&#8203;sluongng](https://togithub.com/sluongng) in [bazelbuild/bazel-gazelle#1197
-   Add -strict to exit on build file and directive errors by [@&#8203;dr-dime](https://togithub.com/dr-dime) in [bazelbuild/bazel-gazelle#1214
-   fix(lang/proto): include imports from different targets by [@&#8203;nickgooding](https://togithub.com/nickgooding) in [bazelbuild/bazel-gazelle#1237
-   Update rules example in README to v0.25.0 by [@&#8203;yujunz](https://togithub.com/yujunz) in [bazelbuild/bazel-gazelle#1240
-   Allow static dependency resolution for Gazelle rule by [@&#8203;uhthomas](https://togithub.com/uhthomas) in [bazelbuild/bazel-gazelle#1242
-   Handle wrapped errors by [@&#8203;illicitonion](https://togithub.com/illicitonion) in [bazelbuild/bazel-gazelle#1234
-   Go: Update tests to use cmp.Diff instead of reflect.DeepEqual by [@&#8203;thempatel](https://togithub.com/thempatel) in [bazelbuild/bazel-gazelle#1244
-   Fix startup script manifest resolution with --nolegacy_external_runfiles by [@&#8203;jvolkman](https://togithub.com/jvolkman) in [bazelbuild/bazel-gazelle#1247
-   Label's package may contain [@&#8203;s](https://togithub.com/s) by [@&#8203;illicitonion](https://togithub.com/illicitonion) in [bazelbuild/bazel-gazelle#1249
-   Trim runfiles prefix consistently by [@&#8203;uhthomas](https://togithub.com/uhthomas) in [bazelbuild/bazel-gazelle#1257
-   Respect .bazelignore by [@&#8203;Whoaa512](https://togithub.com/Whoaa512) in [bazelbuild/bazel-gazelle#1245
-   Implement very minimalistic support for go workspaces by [@&#8203;HakanSunay](https://togithub.com/HakanSunay) in [bazelbuild/bazel-gazelle#1250
-   Fix typo in comment by [@&#8203;yujunz](https://togithub.com/yujunz) in [bazelbuild/bazel-gazelle#1270
-   Use `patch` from `@bazel_tools//tools/build_defs/repo:utils.bzl` by [@&#8203;bozaro](https://togithub.com/bozaro) in [bazelbuild/bazel-gazelle#1269
-   Update rules_go to 0.33.0 by [@&#8203;fmeum](https://togithub.com/fmeum) in [bazelbuild/bazel-gazelle#1263
-   Add support for auth_patterns in go_repository by [@&#8203;dmivankov](https://togithub.com/dmivankov) in [bazelbuild/bazel-gazelle#1254
-   Sluongng/revert patch by [@&#8203;sluongng](https://togithub.com/sluongng) in [bazelbuild/bazel-gazelle#1277
-   Stop inferring import path for empty packages by [@&#8203;linzhp](https://togithub.com/linzhp) in [bazelbuild/bazel-gazelle#1280
-   Don't exclude spaces from the label name regex by [@&#8203;illicitonion](https://togithub.com/illicitonion) in [bazelbuild/bazel-gazelle#1271

#### New Contributors

-   [@&#8203;Anthony-Bible](https://togithub.com/Anthony-Bible) made their first contribution in [bazelbuild/bazel-gazelle#1222
-   [@&#8203;dr-dime](https://togithub.com/dr-dime) made their first contribution in [bazelbuild/bazel-gazelle#1231
-   [@&#8203;sluongng](https://togithub.com/sluongng) made their first contribution in [bazelbuild/bazel-gazelle#1197
-   [@&#8203;nickgooding](https://togithub.com/nickgooding) made their first contribution in [bazelbuild/bazel-gazelle#1237
-   [@&#8203;yujunz](https://togithub.com/yujunz) made their first contribution in [bazelbuild/bazel-gazelle#1240
-   [@&#8203;uhthomas](https://togithub.com/uhthomas) made their first contribution in [bazelbuild/bazel-gazelle#1242
-   [@&#8203;thempatel](https://togithub.com/thempatel) made their first contribution in [bazelbuild/bazel-gazelle#1244
-   [@&#8203;Whoaa512](https://togithub.com/Whoaa512) made their first contribution in [bazelbuild/bazel-gazelle#1245
-   [@&#8203;HakanSunay](https://togithub.com/HakanSunay) made their first contribution in [bazelbuild/bazel-gazelle#1250
-   [@&#8203;bozaro](https://togithub.com/bozaro) made their first contribution in [bazelbuild/bazel-gazelle#1269
-   [@&#8203;fmeum](https://togithub.com/fmeum) made their first contribution in [bazelbuild/bazel-gazelle#1263
-   [@&#8203;dmivankov](https://togithub.com/dmivankov) made their first contribution in [bazelbuild/bazel-gazelle#1254

**Full Changelog**: bazelbuild/bazel-gazelle@v0.25.0...v0.26.0

#### `WORKSPACE` code

    load("@&#8203;bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

    http_archive(
        name = "bazel_gazelle",
        sha256 = "501deb3d5695ab658e82f6f6f549ba681ea3ca2a5fb7911154b5aa45596183fa",
        urls = [
            "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.26.0/bazel-gazelle-v0.26.0.tar.gz",
            "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.26.0/bazel-gazelle-v0.26.0.tar.gz",
        ],
    )

    load("@&#8203;bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")

    ############################################################

### Define your own dependencies here using go_repository.

### Else, dependencies declared by rules_go/gazelle will be used.

### The first declaration of an external repository "wins".

    ############################################################

    gazelle_dependencies()

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/gapic-config-validator).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

go:build syntax is not supported
2 participants