skip to content

Go's testing package has no assertEqual - how do you compare a value against an expected one in a test?

level: juniorimportance: must knowfreq 70%

answer

  1. no assert helpers ship with Go
  2. the comparison is an ordinary if
  3. print both values, not a verdict
  4. got first, then want
  5. name the call and its inputs

basics

~20 s

You compare with ordinary Go code: == for comparable types, reflect.DeepEqual or slices.Equal for composites. On a mismatch call t.Errorf and print both values, conventionally the value you got first, then the value you wanted.

solid answer

~40 s

Go's `testing` package deliberately ships no assertion helpers, so a comparison is just an `if`. Pick the comparison that fits the type: `==` works for comparable types such as numbers, strings and structs whose fields are all comparable; slices, maps and functions are not comparable, so they need `reflect.DeepEqual` or, for slices of comparable elements, `slices.Equal`. When the comparison fails, call `t.Errorf` with a message that names the call, the inputs that identify the case, the value you got and the value you wanted - the house style is `Total(items) = 499, want 500`. Printing both sides is the whole point: a message that only says "mismatch" forces the next reader to reproduce the failure locally before they can start. Use `t.Fatalf` instead when carrying on would only produce noise.

code

go · 7 lines
go
func TestTotal(t *testing.T) {
	items := []Item{{Cents: 199}, {Cents: 301}}
	got := Total(items)
	if want := 500; got != want {
		t.Errorf("Total(%v) = %d, want %d", items, got, want)
	}
}

go deeper

for a junior

Be ready to write the assertion from memory: an if that compares, then t.Errorf printing the call, the value you got and the value you wanted. Interviewers are checking you know no assert helper ships with Go.

for a middle

Expect to explain which comparison fits which type - == for comparable values, reflect.DeepEqual or slices.Equal for slices and maps - and why == on a struct holding a slice does not compile at all.

for a senior

Show that you treat the failure message as the deliverable: someone reading a CI log on another team should identify the case and the difference without rerunning anything. Say what you put in and what you leave out.

for a principal

Own the consistency argument. A package whose tests all report mismatches the same way is greppable and diffable across years; be ready to say when a shared comparison helper is worth its maintenance and when it just hides the message.

## Go has no assertion vocabulary, on purpose Many test frameworks give you a sentence-shaped API - `assertEquals`, `expect(x).toBe(y)`. Go's standard `testing` package gives you none of that. A test is a plain function, a comparison is a plain `if`, and reporting a failure is a call to `t.Errorf` or `t.Fatalf`. Nothing is magic, which means nothing is hidden - and it also means the quality of a test's failure output is entirely on the person who wrote it. That splits the job into two decisions: **which comparison**, and **what the failure message says**. ## Choosing the comparison - **`==`** works for *comparable* types: numbers, strings, booleans, pointers, channels, interfaces, and structs or arrays whose fields are all themselves comparable. This is the default and you should reach for it first - it is exact, it is fast, and it is checked at compile time. - **A struct containing a slice, a map or a function is not comparable**, and `got == want` on one does not compile. That compile error is the usual reason a test author reaches for something else. - **`reflect.DeepEqual(got, want)`** walks two values recursively and reports whether they are deeply equal. It works on anything, which is exactly why it needs care: it compares every field, including unexported ones, it treats a nil slice and an empty non-nil slice as different, and it reports false for two `NaN` float values. - **`slices.Equal(got, want)`** compares two slices of comparable elements element by element. It is checked at compile time, it is cheaper than reflection, and it reports true for any two length-zero slices. `maps.Equal` is its counterpart for maps. - **Compare one field** when only one field is the point of the case. A narrow assertion produces a narrow failure message. ## Writing the failure message The convention throughout the standard library's own tests is to make the message read like the expression that was evaluated, followed by the expectation: ``` Total(items) = 499, want 500 Apply(order, "DE") = 119, want 120 ``` Three things earn their place in that line: 1. **The call and the inputs that identify the case.** Someone reading a CI log should know which case failed without opening the file. 2. **The value you got.** First, because the reader is looking for what the code did. 3. **The value you wanted.** Second, after `want`. The verb matters less than the consistency. A package where half the messages read `want 500, got 499` and half read the other way forces every reader to work out which number is which, and a reversed message sends people hunting for a bug on the wrong side. Pick the verb to match the value: - `%v` for most values. - `%q` for strings, so trailing whitespace and empty strings are visible. - `%+v` for structs, so field names appear. - `%#v` when the two sides print identically under `%v` - a nil slice and an empty slice both print as `[]`, and a message reading `got [], want []` helps nobody. ## Error or Fatal `t.Errorf` records the failure and lets the function continue, so a test that checks several independent things reports all of them in one run. `t.Fatalf` stops the test function immediately - the right choice when continuing would dereference something you just failed to obtain, because the follow-on failures would be noise. ## What this looks like in a library other teams import Suppose you maintain a small money and tax library and `Total` returns the order total in minor units: ```go func TestTotal(t *testing.T) { items := []Item{{Cents: 199}, {Cents: 301}} got := Total(items) if want := 500; got != want { t.Errorf("Total(%v) = %d, want %d", items, got, want) } } ``` The comparison is one line. The message tells a reader on a different team, reading a failed CI job on a dependency bump, exactly what was fed in and what came back. That is the entire craft here: Go gives you no assertion helper, so the assertion and its message are yours to write well. ## Anti-patterns worth naming - `t.Error("mismatch")` - true and useless. - Comparing formatted strings (`fmt.Sprint(got) == fmt.Sprint(want)`) instead of values, which silently equates values of different types. - Panicking instead of reporting, which loses the test framework's file and line attribution. - Reversing got and want in some tests but not others.

  • Why does the convention put the value you got before the value you wanted?
    It makes the message read like the code that was executed: the call, then what it produced, then the expectation. Consistency matters more than the choice itself. A package that mixes both orders forces every reader to work out which number is which, and one reversed message sends people looking for a bug on the wrong side of the comparison.
  • What belongs in the message besides the two values?
    The call and the inputs that identify the case, so `Apply(order, "DE") = 119, want 120` tells a reader in a CI log which case failed without opening the file. Keep the values raw rather than describing them in prose - "the total was wrong" discards the one thing the reader needs. If the test is a named subtest, the case name already carries some of that context.
  • Would you compare a whole struct or field by field?
    Compare the whole value when every field is part of the contract you mean to pin: one comparison, one message. Compare field by field when only some fields matter, when others hold timestamps or generated ids, or when a whole-struct message would be unreadable. A middle path is to build a reduced value holding only the fields you are asserting, then compare that.

A failure line is a bug report written in advance: it should say what was run and what came back, not merely that something went wrong.

saying these in an interview costs you the question

  • Claims Go tests cannot compare values without an external assertion library
  • Reports only that the test failed, printing neither value
  • Prints want before got so every message in the package reads backwards
  • Uses == on a struct containing a slice and is surprised it does not compile
  • Compares fmt.Sprint output instead of the values themselves