Skip to content

proposal: testing/cmp: add new package #45200

@dsnet

Description

@dsnet

TL;DR, I propose adding github.com/google/go-cmp/cmp to the standard library as testing/cmp.

Determining equality of two values is one of the most common operations needed for a unit test where the test wants to know whether some computed value matches some pre-determined expected value.

Using the Go language and standard library alone, there are two primary options:

For simple cases, these work fine, but are insufficient for more complex cases:

  • The == operator only works on comparable types, which means that it doesn't work for Go slices and maps, which are two common kinds that tests want to compare.
  • The reflect.DeepEqual function is a "recursive relaxation of Go's == operator", but has several short-comings:
    • It provides no ability to customize the comparison, which is increasingly more useful for more complex types.
    • It does not explain why two values are different, which becomes increasingly more helpful for larger values.
    • It blindly compares unexported fields, which causes a test to unfortunately have an implicit dependency on unexported artifacts of types from a module's dependencies (and therefore on undefined behavior).

For many users, the standard library is insufficient, so they turn to third-party packages to perform equality. According to the module proxy, the most common comparison module used is github.com/google/go-cmp with 7264 module dependents (and 25th most imported module). I propose including cmp in the standard library itself.

Why include cmp in the standard library?

The most widely used comparison function in Go is currently reflect.DeepEqual (with ~34k usages of reflect.DeepEqual compared to ~6k usages of cmp.Equal). Inclusion of cmp to the standard library would provide better visibility to it and allow more tests to be written that would have been better off using cmp.Equal rather than reflect.DeepEqual.

A problem with reflect.DeepEqual is that it hampers module authors from making changes to their types that otherwise should be safe to perform. Since reflect.DeepEqual blindly compares unexported fields, it causes a test to have an implicit dependency on the value of unexported fields in types that come from a module's dependency. When the author of one those types changes an unexported field, it surprisingly breaks many brittle tests. Examples of this occurring was when Go1.9 added monotonic timestamp support and the change to the internal representation of time.Time broke hundreds of test at Google. Similar problems occurred with adding/modifying unexported fields to protocol buffer messages. reflect.DeepEqual is a comparison function that looks like it works wells, but may cause the test to be brittle. Furthermore, reflect.DeepEqual does not tell you why two values are different, making it even more challenging for users to diagnose such brittle tests.

As a contributor to the standard library, there are a number of times that I would have liked to use cmp when writing tests in the standard library itself.

How is cmp.Equal similar or different than reflect.DeepEqual?

cmp.Equal is designed to be more expressive than reflect.DeepEqual. It accepts any number of user-specified options that may affect how the comparison is performed. While reflect.DeepEqual is more performant, cmp.Equal is more flexible. Also, cmp.Diff provides a humanly readable report for why two values differ, rather than simply providing a boolean result for whether they differ.

One significant difference from reflect.DeepEqual is that cmp.Equal panics by default when trying to compare unexported fields unless the user explicitly permits it with an cmp.Exporter option. This design decision was to avoid the problems observed with using reflect.DeepEqual at scale mentioned earlier.

Without any options specified, cmp.Equal is identical to reflect.DeepEqual except:

  • cmp.Equal panics when trying to comparing unexported fields
  • cmp.Equal uses a type's Equal method if it has one
  • cmp.Equal has an arguably more correct comparison for graphs
    • reflect.DeepEqual's cycle detection primarily aims to avoid infinite recursion, but does not necessarily verify whether the two graphs have the same topology, while cmp.Equal checks that the graph topology are the same.

(Fun fact) Package reflect is the 15th most imported Go package, but ~77% of the imports (for test files) only do so to use DeepEqual. In 2011, Rob Pike explained that "[reflection] is a powerful tool that should be used with care and avoided unless strictly necessary." I find it ironic that most usages of reflect is to access a function that is arguably not even about Go reflection (i.e., it doesn't provide functionality that mirrors the Go language itself).

How does cmp compare to other comparison packages?

The only other comparison module (within the top 250 most widely used modules) is github.com/go-test/deep with 845 dependents (compared to 7264 dependents for cmp). Package deep is not as flexible as cmp and relies on globals to configure how comparisons are performed.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions