Question
In Go, I am running a test that prints a value for debugging, but nothing appears in the output.
For example:
func TestPrintSomething(t *testing.T) {
fmt.Println("Say hi")
}
When I run:
go test
I only see:
ok command-line-arguments 0.004s
If I use t.Error() instead:
func TestPrintSomethingAgain(t *testing.T) {
t.Error("Say hi")
}
then the message is printed, but the test fails.
How can I print debugging information in a Go test using the testing package without causing the test to fail?
Short Answer
By the end of this page, you will understand why fmt.Println output often does not appear during go test, how Go captures test output, and the correct ways to print debugging information using t.Log, t.Logf, and the -v flag.
Concept
In Go tests, output is handled differently from normal programs. When you run go test, the testing tool captures standard output and standard error produced during test execution.
This means:
fmt.Println()may not appear in the terminal for passing tests.t.Error()prints because it records a test failure.t.Log()andt.Logf()are the intended tools for test-specific debug messages.
Why does Go do this?
Because test output should usually stay clean unless:
- a test fails, or
- you explicitly ask for verbose output.
This makes large test suites easier to read. If every passing test printed debug text, the output would quickly become noisy.
The most important rule is:
- Use
t.Log()ort.Logf()for debugging inside tests. - Run tests with
go test -vif you want to see log output from passing tests.
Example:
func TestPrintSomething(t *testing.T) {
t.Log("Say hi")
}
Run with:
go test -v
Then you will see the message.
In short, the issue is not that printing is broken. It is that Go test intentionally hides most output unless it is relevant.
Mental Model
Think of go test as a quiet test supervisor.
fmt.Println()is like speaking into the room.t.Log()is like writing a note into the test report.t.Error()is like raising a red flag.
If everything passes, the supervisor keeps the room quiet and only gives a short summary.
If you ask for a detailed report with -v, it shows the notes too.
If something fails, the red flags are always shown.
So the key idea is: Go tests collect information first, then decide what to display.
Syntax and Examples
The main tools are:
fmt.Println
func TestWithFmt(t *testing.T) {
fmt.Println("debug message")
}
- Writes to standard output.
- Often hidden for passing tests.
- May appear when tests fail or in some environments, but you should not rely on it for test logs.
t.Log
func TestWithLog(t *testing.T) {
t.Log("debug message")
}
- Adds a message to the test log.
- Shown automatically when the test fails.
- Shown for passing tests only with
go test -v.
t.Logf
func TestWithLogf(t *testing.T) {
name := "Gopher"
t.Logf("hello, %s", name)
}
- Same as
t.Log, but supports formatting. - Useful when you want to print variable values.
t.Error
func TestWithError(t *testing.T) {
t.Error("something went wrong")
}
- Logs a message.
- Marks the test as failed.
- Use this only when the test should actually fail.
Best beginner-friendly example
package main
import "testing"
func TestGreeting(t *testing.T) {
t.Log("Starting greeting test")
got := "hello"
want := "hello"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
Run normally:
go test
Output will usually be minimal if the test passes.
Run verbosely:
go test -v
Now you will see the t.Log message as well as the test execution details.
Step by Step Execution
Consider this test:
func TestPrintSomething(t *testing.T) {
t.Log("Say hi")
}
Now run:
go test -v
What happens step by step:
go testbuilds a temporary test binary.- The test runner finds functions whose names start with
Testand match the required signature. TestPrintSomethingis executed.t.Log("Say hi")stores a log message in the test context.- The test finishes without failure.
- Because
-vwas used, Go prints logs even for passing tests.
Typical output:
=== RUN TestPrintSomething
foo_test.go:6: Say hi
--- PASS: TestPrintSomething (0.00s)
PASS
ok example 0.003s
Now compare with running without -v:
go test
Step 4 still happens, but since the test passes and verbose mode is off, the log is not shown.
If the test fails:
func TestPrintSomething(t *testing.T) {
t.Log("Say hi")
t.Fatal("forcing failure")
}
Then the log message is shown because the test failed, even without -v.
Real World Use Cases
This behavior is useful in real testing work.
Debugging a failing unit test
func TestAdd(t *testing.T) {
got := 2 + 2
t.Logf("computed result: %d", got)
if got != 4 {
t.Fatalf("expected 4, got %d", got)
}
}
You can inspect intermediate values without permanently cluttering normal test output.
Inspecting API responses in tests
func TestAPIResponse(t *testing.T) {
body := `{"status":"ok"}`
t.Logf("response body: %s", body)
}
This helps when checking parsed JSON, headers, or mock server responses.
Verifying table-driven tests
func TestMultiply(t *testing.T) {
tests := []struct {
a, b int
want int
}{
{2, 3, 6},
{4, 5, 20},
}
for _, tc := range tests {
t.Logf("testing %d * %d", tc.a, tc.b)
got := tc.a * tc.b
if got != tc.want {
t.Fatalf("got %d, want %d", got, tc.want)
}
}
}
Logs can help identify which case was running when a failure occurred.
Temporary debugging during development
Developers often add t.Logf() while investigating a bug, run go test -v, then remove or keep the logs if they are still useful.
Real Codebase Usage
In real Go codebases, developers usually prefer t.Log and t.Logf over fmt.Println inside tests.
Common patterns include:
Logging useful context before an assertion
func TestUserID(t *testing.T) {
id := 42
t.Logf("generated user id: %d", id)
if id <= 0 {
t.Fatal("user id must be positive")
}
}
This helps when a CI failure needs quick diagnosis.
Using guard clauses with t.Fatal
func TestConfig(t *testing.T) {
cfg, err := loadConfig()
if err != nil {
t.Fatalf("loadConfig failed: %v", err)
}
t.Logf("loaded config: %+v", cfg)
}
If setup fails, the test stops immediately.
Logging inside table-driven tests
func TestNormalize(t *testing.T) {
tests := []struct {
input string
want string
}{
{" Hello ", "hello"},
{"Go", "go"},
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
t.Logf("input=%q", tc.input)
got := normalize(tc.input)
if got != tc.want {
t.Fatalf("got %q, want %q", got, tc.want)
}
})
}
}
This works especially well with subtests.
Avoiding noisy output in CI
Most teams keep normal test output quiet:
go testfor routine runsgo test -vfor investigationt.Logffor contextual test loggingt.Errorfort.Fatalffor actual failures
This keeps pipelines readable while still making debugging possible.
Common Mistakes
1. Using fmt.Println and expecting it to always appear
Broken expectation:
func TestSomething(t *testing.T) {
fmt.Println("hello")
}
Why it is a problem:
go testmay capture and hide this output for passing tests.
Better:
func TestSomething(t *testing.T) {
t.Log("hello")
}
Run with:
go test -v
2. Using t.Error just to print a message
Broken usage:
func TestSomething(t *testing.T) {
t.Error("debug value")
}
Why it is a problem:
t.Errormarks the test as failed.- It should be used only when the test result is actually wrong.
Better:
func TestSomething(t *testing.T) {
t.Log("debug value")
}
3. Forgetting the -v flag
If your test passes, this:
func TestSomething(t *testing.T) {
t.Log("visible only in verbose mode for passing tests")
}
may show nothing with:
go test
Use:
go test -v
4. Confusing t.Error, t.Fail, t.Fatal, and t.Log
t.Log: log onlyt.Error: log and failt.Fatal: log, fail, and stop the test immediatelyt.Fail: fail without a message
5. Leaving too much debug noise in tests
Even though t.Log is useful, excessive logging can make failure output harder to read. Keep logs focused on values that help diagnose problems.
Comparisons
| Tool | Prints a message | Fails the test | Stops execution immediately | Shown for passing tests by default |
|---|---|---|---|---|
fmt.Println | Yes | No | No | Usually no |
t.Log | Yes | No | No | No |
t.Logf | Yes | No | No | No |
t.Error | Yes | Yes | No | Yes, because test fails |
t.Errorf | Yes | Yes | No | Yes, because test fails |
t.Fatal | Yes | Yes | Yes | Yes, because test fails |
t.Fatalf | Yes | Yes | Yes | Yes, because test fails |
fmt.Println vs t.Log
| Aspect | fmt.Println | t.Log |
|---|---|---|
| Intended for tests | Not specifically | Yes |
| Connected to current test | No | Yes |
| Hidden/shown by test runner rules | Less predictable | Designed for that system |
| Best for debugging tests | Not ideal | Recommended |
Rule of thumb
- Use
t.Log/t.Logffor debugging information. - Use
t.Error/t.Errorfwhen the test should fail. - Use
t.Fatal/t.Fatalfwhen the test cannot continue.
Cheat Sheet
// Log without failing
func TestExample(t *testing.T) {
t.Log("message")
t.Logf("value: %d", 42)
}
// Fail but continue
func TestExample(t *testing.T) {
t.Error("something is wrong")
t.Errorf("got %d, want %d", 1, 2)
}
// Fail and stop immediately
func TestExample(t *testing.T) {
t.Fatal("cannot continue")
t.Fatalf("bad input: %q", "")
}
# Default: quiet for passing tests
go test
# Verbose: show logs for passing tests
go test -v
Key rules:
t.Logandt.Logfare the correct ways to print debug info in tests.- Logs from passing tests are usually hidden unless you use
-v. - Logs from failing tests are shown automatically.
t.Errorandt.Fatalboth fail the test.t.Fatalstops the current test immediately.fmt.Printlnis not the recommended way to log inside tests.
FAQ
Why does fmt.Println not show output in go test?
Because the Go test runner captures test output and usually hides it for passing tests to keep output clean.
How do I print debug information in a Go test without failing it?
Use t.Log() or t.Logf().
Why does t.Error() print the message?
Because it records a failure. Go shows failure output even when verbose mode is off.
How do I see t.Log() output for successful tests?
Run tests with:
go test -v
Should I use fmt.Println or t.Log in tests?
Prefer t.Log because it is designed for the testing package and works with Go's test reporting.
What is the difference between t.Error and t.Fatal?
t.Error marks the test as failed but continues running. t.Fatal marks the test as failed and stops that test immediately.
Are t.Log messages shown when a test fails?
Yes. Logs are shown for failed tests even without -v.
Can I use formatted output in test logs?
Yes. Use t.Logf() for formatted messages, just like fmt.Printf.
Mini Project
Description
Create a small Go test file that demonstrates the difference between fmt.Println, t.Log, and t.Error. This helps you understand what appears during a normal go test run versus a verbose go test -v run.
Goal
Write tests that show how Go handles regular output, test logs, and test failures.
Requirements
Create one test that uses fmt.Println.
Create one test that uses t.Log.
Create one test that uses t.Error.
Run the tests with both go test and go test -v and observe the differences.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.