πŸ“‹ How to Keep Go Unit Test Logs Clean: Only Log Failed Inputs πŸ”—

When writing unit tests in Goβ€”especially table-driven testsβ€”functions that write to the standard global log package can quickly clutter your terminal.

If you have a dozen test cases and only one fails, you still have to scroll through the log output of all the successful cases just to find the error. This noise slows down debugging and makes your test output difficult to read.

Let’s look at a common scenario and how to fix it by capturing logs and printing them only when a test fails.

πŸ“‘ Table of Contents πŸ”—


πŸ”Š The Problem: Noisy Test Output πŸ”—

Say we have a simple function that logs its inputs and its final result:

package add

import (
	"log"
)

func add(x, y int) int {
	log.Printf("add %d %d", x, y)
	res := x + y
	log.Printf("add %d %d = %d", x, y, res)
	return res
}

Now, we write a standard table-driven unit test with three cases (where the last one is intentionally designed to fail):

package add

import (
	"testing"
)

func TestAdd(t *testing.T) {
	inputs := []struct {
		x    int
		y    int
		want int
	}{
		{x: 1, y: 2, want: 3},
		{x: 3, y: 2, want: 5},
		{x: 4, y: 5, want: 0}, // Intentionally failing case
	}

	for _, in := range inputs {
		got := add(in.x, in.y)
		if in.want != got {
			t.Errorf("add %d %d got %d want %d", in.x, in.y, got, in.want)
		}
	}
}

When we run go test, the output contains the logs of all passing inputs before finally showing us the failure:

$ go test
2020/09/05 09:01:25 add 1 2
2020/09/05 09:01:25 add 1 2 = 3
2020/09/05 09:01:25 add 3 2
2020/09/05 09:01:25 add 3 2 = 5
2020/09/05 09:01:25 add 4 5
2020/09/05 09:01:25 add 4 5 = 9
--- FAIL: TestAdd (0.00s)
    add_test.go:28: add 4 5 got 9 want 0
FAIL
exit status 1

πŸ› οΈ The Solution: Dependency Injection and Buffers πŸ”—

It would be much better to only see the logs generated by the failing inputs. We can achieve this by passing a logger into our function (Dependency Injection) and writing the logs to a temporary buffer during our tests.

First, let’s update our function to accept a *log.Logger:

package add

import "log"

func add(logger *log.Logger, x, y int) int {
	logger.Printf("add %d %d", x, y)
	res := x + y
	logger.Printf("add %d %d = %d", x, y, res)
	return res
}

Next, we update our test. For every test case iteration, we will create a fresh bytes.Buffer and attach a new logger to it. If the test passes, the buffer is simply discarded. If the test fails, we print the buffer’s contents alongside the error message!

(Note: We are also upgrading the test to use t.Run, which is the idiomatic way to run table-driven subtests in Go).

package add

import (
	"bytes"
	"log"
	"testing"
)

func TestAdd(t *testing.T) {
	inputs := []struct {
		name string
		x    int
		y    int
		want int
	}{
		{name: "1+2", x: 1, y: 2, want: 3},
		{name: "3+2", x: 3, y: 2, want: 5},
		{name: "4+5", x: 4, y: 5, want: 0}, // Intentionally failing case
	}

	for _, in := range inputs {
		t.Run(in.name, func(t *testing.T) {
			// 1. Create a buffer to capture the logs
			var buf bytes.Buffer
			
			// 2. Create a new logger that writes to the buffer
			logger := log.New(&buf, "", log.LstdFlags)
			
			// 3. Pass the custom logger into our function
			got := add(logger, in.x, in.y)
			
			if in.want != got {
				// 4. Print the buffer contents ONLY if the test fails
				t.Errorf("got %d want %d\n\nExecution Logs:\n%s", got, in.want, buf.String())
			}
		})
	}
}

✨ The Clean Output πŸ”—

Now, when we run our tests, the output is perfectly clean and highly actionable. We only see the logs that are relevant to the failure:

$ go test
--- FAIL: TestAdd (0.00s)
    --- FAIL: TestAdd/4+5 (0.00s)
        add_test.go:35: got 9 want 0
            
            Execution Logs:
            2020/09/05 09:22:48 add 4 5
            2020/09/05 09:22:48 add 4 5 = 9
            
FAIL
exit status 1

By decoupling our functions from the global logger, we make our code more flexible, our test suites quieter, and our debugging process significantly faster.


πŸ“š Download My Free Ebook πŸ”—

Get the curated offline edition of Kaue's Way of Lifeβ€”a practical field guide to optimizing 🍏 Health, πŸ’° Wealth, ❀️ Love, and 🧘 Meaning.

Kaue's Way of Life

Kaue's Way of Life
Minimum price
Free!
Suggested price
$0.00

πŸ“¬ Join My Mailing List πŸ”—

Get notified when I publish new essays, book summaries, and practical guides on living well. No spamβ€”unsubscribe anytime.


πŸ’¬ Comments & Discussion πŸ”—

Have a question, perspective, or suggestion? Share your thoughts below.



Updated on 2026 Oct 11.

DISCLAIMER: The content on this site is for informational and educational purposes only and does not constitute medical, psychological, financial, investment, tax, or legal advice. The ideas, curation, and opinions presented here are my own and do not represent those of my employer. Book, lecture, and article summaries are independent educational commentary and remain the intellectual property of their respective authors. AI tools may be used to assist with editing, formatting, and synthesis; while every effort is made to ensure accuracy, errors may occur. PRIVACY: This site uses third-party services for cookieless analytics (Google Analytics), optional newsletter subscriptions (Mailchimp), comments (Disqus), live chat (Tawk.to), and embedded media (YouTube), which may process visitor IP addresses and cookies in accordance with their respective privacy policies.