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.
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
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())
}
})
}
}
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.
Get the curated offline edition of Kaue's Way of Lifeβa practical field guide to optimizing π Health, π° Wealth, β€οΈ Love, and π§ Meaning.
Get notified when I publish new essays, book summaries, and practical guides on living well. No spamβunsubscribe anytime.
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.