Skip to content
gohacks.dev
Testing

Testify Failure Messages and Diff Output Made Readable

Read testify's Equal failure output: the expected and actual lines, the unified diff of a nested struct and map, a long string, and how to shorten it.

4 min read
Summarize in AI
Flat illustration of a test report with one failing test.

The first time you see a testify failure for a large struct, it is a wall of text. The second time, you know where to look. This short note walks through the format on Go 1.25.6, testify v1.11.1, and gives a way to read it quickly.

Tested with Go 1.25.6, testify v1.11.1, macOS on Apple Silicon (arm64)

diff_test.goGo
package main

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

type Order struct {
	ID    string
	Lines []Line
	Meta  map[string]string
}
type Line struct {
	SKU string
	Qty int
}

func TestStructDiff(t *testing.T) {
	want := Order{"o-1", []Line{{"pen", 2}, {"book", 1}}, map[string]string{"channel": "web"}}
	got := Order{"o-1", []Line{{"pen", 2}, {"book", 3}}, map[string]string{"channel": "app"}}
	assert.Equal(t, want, got)
}

func TestLongStringDiff(t *testing.T) {
	want := strings.Repeat("alpha\n", 4) + "beta\n" + strings.Repeat("gamma\n", 4)
	got := strings.Repeat("alpha\n", 4) + "BETA\n" + strings.Repeat("gamma\n", 4)
	assert.Equal(t, want, got)
}

func main() {}
Shell
go test -count=1 -v . 2>&1 | grep -vE '^(=== RUN|=== PAUSE|=== CONT|=== NAME|PASS|ok|FAIL$|FAIL\s|exit status)' | sed -E 's/ \([0-9.]+s\)//; s#^ +[a-z_0-9]+\.go:[0-9]+: #    #; s/\x1b\[[0-9;]*m//g; s#/[^[:space:]]*/([a-z_0-9]+\.go)#\1#g; s/[[:space:]]+$//' | grep -vE '^\s*$'
Text
        	Error Trace:	diff_test.go:23
        	Error:      	Not equal:
        	            	expected: main.Order{ID:"o-1", Lines:[]main.Line{main.Line{SKU:"pen", Qty:2}, main.Line{SKU:"book", Qty:1}}, Meta:map[string]string{"channel":"web"}}
        	            	actual  : main.Order{ID:"o-1", Lines:[]main.Line{main.Line{SKU:"pen", Qty:2}, main.Line{SKU:"book", Qty:3}}, Meta:map[string]string{"channel":"app"}}
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -9,3 +9,3 @@
        	            	    SKU: (string) (len=4) "book",
        	            	-   Qty: (int) 1
        	            	+   Qty: (int) 3
        	            	   }
        	            	@@ -13,3 +13,3 @@
        	            	  Meta: (map[string]string) (len=1) {
        	            	-  (string) (len=7) "channel": (string) (len=3) "web"
        	            	+  (string) (len=7) "channel": (string) (len=3) "app"
        	            	  }
        	Test:       	TestStructDiff
--- FAIL: TestStructDiff
        	Error Trace:	diff_test.go:29
        	Error:      	Not equal:
        	            	expected: "alpha\nalpha\nalpha\nalpha\nbeta\ngamma\ngamma\ngamma\ngamma\n"
        	            	actual  : "alpha\nalpha\nalpha\nalpha\nBETA\ngamma\ngamma\ngamma\ngamma\n"
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -4,3 +4,3 @@
        	            	 alpha
        	            	-beta
        	            	+BETA
        	            	 gamma
        	Test:       	TestLongStringDiff
--- FAIL: TestLongStringDiff

Reading a struct diff

The failure has four parts: the Error Trace line (the assertion's file and line), Error: Not equal:, then expected: and actual : on single lines, then Diff:. The single-line forms are Go syntax from %#v, which is long for nested values, and reading them for a difference is slow. Go to the diff instead.

The diff is in unified format, produced from a pretty-printed dump of both values. The header says --- Expected and +++ Actual. A hunk starts with @@ -9,3 +9,3 @@, meaning three lines starting at line 9 of the expected dump against three at line 9 of the actual. Lines that begin with - exist only in the expected value, lines with + only in the actual one, and lines with a leading space are unchanged context.

In the struct test there are two hunks. The first shows - Qty: (int) 1 and + Qty: (int) 3 under the line SKU: (string) (len=4) "book", so the second line item's quantity differs. The second hunk shows the map entry "channel": "web" against "app". Reading the context lines above each change tells you which element of the slice and which key of the map: the dump has the indentation and the field names.

Strings

For multi-line strings, the diff compares line by line. The long string test has four alpha lines, one beta against BETA and four gamma lines. The hunk @@ -4,3 +4,3 @@ shows alpha, -beta, +BETA, gamma: one line of context on each side and the change. It is the same view as git diff, and it turns a nine-line mismatch into a four-line report. The expected: and actual : lines above are the escaped strings, with the newline characters shown as escapes, which is useful when the difference is whitespace.

Types in the dump

The pretty-printed form prints each value with its type and, for strings, its length: (string) (len=4) "book", (int) 1. Those annotations are not decoration. A failure where the visible text looks identical often comes down to a type or length difference: an int against an int64, a string with a trailing space (the lengths differ by one), a []byte against a string. If the diff shows a changed line that reads the same to you, compare the type and length in parentheses before looking elsewhere.

Making it shorter

  • Compare a projection. If one field matters, assert on it: assert.Equal(t, want.Qty, got.Lines[1].Qty). The failure is two numbers.
  • Use ElementsMatch for unordered slices, which reports the elements missing and extra, not a positional diff.
  • Use JSONEq for JSON. It reports the two documents formatted.
  • Print the diff, not the dump. go-cmp prints only the differing parts; see the libraries compared.
  • Shrink the data. Tests with small, purposeful fixtures produce small failures.

The format is produced by testify's assert package; see the assertions tutorial and the package documentation.

Taggedquick tip
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.