Overview
CaptureBuffer is a thread-safe io.Writer implementation that captures log output in memory for testing. Unlike MockLogger (which captures structured entries), CaptureBuffer captures the raw formatted output, making it ideal for integration tests and formatter verification.
Creating a CaptureBuffer
package myapp
import (
" testing "
" github.com/drossan/go_logs "
)
func TestLogOutput ( t * testing . T ) {
// Create capture buffer
buf := go_logs . NewCaptureBuffer ()
// Create logger that writes to buffer
logger , _ := go_logs . New (
go_logs . WithOutput ( buf ),
go_logs . WithFormatter ( go_logs . NewTextFormatter ()),
)
logger . Info ( "test message" )
// Verify output contains expected text
if ! buf . Contains ( "test message" ) {
t . Error ( "expected 'test message' in output" )
}
}
API Reference
Creation
Creates a new thread-safe CaptureBuffer with:
Empty internal buffer
Mutex for concurrent access
Full io.Writer implementation
Core Methods
Write()
Implements io.Writer interface (automatically called by logger):
buf := go_logs . NewCaptureBuffer ()
n , err := buf . Write ([] byte ( "log line \n " ))
String()
Returns the entire buffer contents as a string:
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New ( go_logs . WithOutput ( buf ))
logger . Info ( "first" )
logger . Info ( "second" )
output := buf . String ()
fmt . Println ( output )
// [2026/03/03 12:00:00] INFO first
// [2026/03/03 12:00:01] INFO second
Bytes()
Returns the buffer contents as a byte slice:
data := buf . Bytes ()
if len ( data ) == 0 {
t . Error ( "expected log output" )
}
Reset()
Clears the buffer completely:
buf . Write ([] byte ( "old data" ))
assert . True ( t , buf . Contains ( "old data" ))
buf . Reset ()
assert . Equal ( t , "" , buf . String ())
assert . Equal ( t , 0 , buf . LineCount ())
Search Methods
Contains()
Checks if buffer contains a substring:
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New ( go_logs . WithOutput ( buf ))
logger . Info ( "user logged in" , go_logs . String ( "username" , "john" ))
if ! buf . Contains ( "user logged in" ) {
t . Error ( "expected 'user logged in' in output" )
}
if ! buf . Contains ( "username=john" ) {
t . Error ( "expected 'username=john' in output" )
}
if buf . Contains ( "password" ) {
t . Error ( "should not contain 'password'" )
}
ContainsAll()
Checks if buffer contains all specified substrings:
logger . Info ( "processing order" ,
go_logs . Int ( "order_id" , 12345 ),
go_logs . Float64 ( "total" , 99.99 ),
)
if ! buf . ContainsAll ( "processing order" , "order_id=12345" , "total=99.99" ) {
t . Error ( "expected all substrings in output" )
}
ContainsAny()
Checks if buffer contains at least one of the specified substrings:
logger . Error ( "database error" , go_logs . Err ( dbErr ))
// Check for any error indicator
if ! buf . ContainsAny ( "ERROR" , "error" , "failed" ) {
t . Error ( "expected error indicator in output" )
}
Line Methods
Lines()
Returns all lines as a slice:
logger . Info ( "line 1" )
logger . Info ( "line 2" )
logger . Info ( "line 3" )
lines := buf . Lines ()
if len ( lines ) != 3 {
t . Errorf ( "expected 3 lines, got %d " , len ( lines ))
}
for i , line := range lines {
fmt . Printf ( "Line %d : %s \n " , i + 1 , line )
}
LastLine()
Returns the most recent line:
logger . Info ( "first log" )
logger . Info ( "second log" )
logger . Error ( "last log" )
last := buf . LastLine ()
if ! strings . Contains ( last , "last log" ) {
t . Errorf ( "expected 'last log', got: %s " , last )
}
if ! strings . Contains ( last , "ERROR" ) {
t . Error ( "last line should contain ERROR level" )
}
LineCount()
Returns the number of lines:
logger . Info ( "log 1" )
logger . Info ( "log 2" )
if buf . LineCount () != 2 {
t . Errorf ( "expected 2 lines, got %d " , buf . LineCount ())
}
Testing Examples
Example 1: Test Text Formatter Output
func TestTextFormatter ( t * testing . T ) {
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New (
go_logs . WithOutput ( buf ),
go_logs . WithFormatter ( go_logs . NewTextFormatter ()),
)
logger . Info ( "test message" , go_logs . String ( "key" , "value" ))
output := buf . String ()
// Verify format: [timestamp] LEVEL message key=value
if ! strings . Contains ( output , "INFO" ) {
t . Error ( "expected INFO level in output" )
}
if ! strings . Contains ( output , "test message" ) {
t . Error ( "expected message in output" )
}
if ! strings . Contains ( output , "key=value" ) {
t . Error ( "expected field in output" )
}
}
func TestJSONFormatter ( t * testing . T ) {
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New (
go_logs . WithOutput ( buf ),
go_logs . WithFormatter ( go_logs . NewJSONFormatter ()),
)
logger . Info ( "json test" ,
go_logs . String ( "service" , "api" ),
go_logs . Int ( "port" , 8080 ),
)
// Parse JSON output
var result map [ string ] interface {}
err := json . Unmarshal ( buf . Bytes (), & result )
if err != nil {
t . Fatalf ( "failed to parse JSON: %v " , err )
}
// Verify JSON structure
if result [ "level" ] != "INFO" {
t . Error ( "expected INFO level" )
}
if result [ "message" ] != "json test" {
t . Error ( "expected 'json test' message" )
}
fields := result [ "fields" ].( map [ string ] interface {})
if fields [ "service" ] != "api" {
t . Error ( "expected service=api" )
}
}
Example 3: Test Multiple Log Lines
func TestMultipleLogLines ( t * testing . T ) {
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New ( go_logs . WithOutput ( buf ))
logger . Info ( "starting" )
logger . Debug ( "processing" ) // May be filtered
logger . Info ( "completed" )
lines := buf . Lines ()
// Verify at least 2 lines (Debug may be filtered)
if len ( lines ) < 2 {
t . Errorf ( "expected at least 2 lines, got %d " , len ( lines ))
}
// Verify first and last lines
if ! strings . Contains ( lines [ 0 ], "starting" ) {
t . Error ( "first line should contain 'starting'" )
}
lastLine := lines [ len ( lines ) - 1 ]
if ! strings . Contains ( lastLine , "completed" ) {
t . Error ( "last line should contain 'completed'" )
}
}
Example 4: Test Level Filtering
func TestLevelFiltering ( t * testing . T ) {
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New (
go_logs . WithOutput ( buf ),
go_logs . WithLevel ( go_logs . WarnLevel ), // Only Warn+
)
logger . Debug ( "debug message" ) // Filtered
logger . Info ( "info message" ) // Filtered
logger . Warn ( "warn message" ) // Logged
logger . Error ( "error message" ) // Logged
output := buf . String ()
// Verify filtered logs are not in output
if buf . Contains ( "debug message" ) {
t . Error ( "debug should be filtered" )
}
if buf . Contains ( "info message" ) {
t . Error ( "info should be filtered" )
}
// Verify non-filtered logs are present
if ! buf . Contains ( "warn message" ) {
t . Error ( "warn should be logged" )
}
if ! buf . Contains ( "error message" ) {
t . Error ( "error should be logged" )
}
// Should have exactly 2 lines
if buf . LineCount () != 2 {
t . Errorf ( "expected 2 lines, got %d " , buf . LineCount ())
}
}
Example 5: Test Structured Fields
func TestStructuredFields ( t * testing . T ) {
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New ( go_logs . WithOutput ( buf ))
err := errors . New ( "connection timeout" )
logger . Error ( "request failed" ,
go_logs . String ( "method" , "GET" ),
go_logs . String ( "path" , "/api/users" ),
go_logs . Int ( "status" , 500 ),
go_logs . Float64 ( "duration" , 5.234 ),
go_logs . Bool ( "retry" , true ),
go_logs . Err ( err ),
)
// Verify all fields are in output
if ! buf . ContainsAll (
"request failed" ,
"method=GET" ,
"path=/api/users" ,
"status=500" ,
"duration=5.234" ,
"retry=true" ,
"error=connection timeout" ,
) {
t . Error ( "expected all fields in output" )
t . Logf ( "Output: %s " , buf . String ())
}
}
Example 6: Integration Test
func TestApplicationStartup ( t * testing . T ) {
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New (
go_logs . WithOutput ( buf ),
go_logs . WithLevel ( go_logs . InfoLevel ),
)
// Simulate application startup
app := NewApplication ( logger )
err := app . Start ()
if err != nil {
t . Fatalf ( "app failed to start: %v " , err )
}
// Verify startup logs
if ! buf . Contains ( "loading configuration" ) {
t . Error ( "expected config loading log" )
}
if ! buf . Contains ( "database connected" ) {
t . Error ( "expected database connection log" )
}
if ! buf . Contains ( "server listening" ) {
t . Error ( "expected server start log" )
}
// Verify startup sequence
lines := buf . Lines ()
if len ( lines ) < 3 {
t . Error ( "expected at least 3 startup logs" )
}
// Last line should confirm ready state
lastLine := buf . LastLine ()
if ! strings . Contains ( lastLine , "ready" ) {
t . Error ( "expected 'ready' in last log" )
}
}
Thread Safety
CaptureBuffer is fully thread-safe using mutex locks:
func TestConcurrentWrites ( t * testing . T ) {
buf := go_logs . NewCaptureBuffer ()
logger , _ := go_logs . New ( go_logs . WithOutput ( buf ))
var wg sync . WaitGroup
for i := 0 ; i < 100 ; i ++ {
wg . Add ( 1 )
go func ( id int ) {
defer wg . Done ()
logger . Info ( "concurrent log" , go_logs . Int ( "id" , id ))
}( i )
}
wg . Wait ()
// All 100 logs should be captured
if buf . LineCount () != 100 {
t . Errorf ( "expected 100 lines, got %d " , buf . LineCount ())
}
}
Combining with Real Logger Tests
From logger_test.go:47-61:
// TestLogger_Log verifies basic Log() functionality
func TestLogger_Log ( t * testing . T ) {
buf := & bytes . Buffer {}
logger , _ := New ( WithOutput ( buf ))
logger . Log ( InfoLevel , "test message" , String ( "key" , "value" ))
output := buf . String ()
if ! strings . Contains ( output , "test message" ) {
t . Error ( "Log() output should contain message" )
}
if ! strings . Contains ( output , "key=value" ) {
t . Error ( "Log() output should contain fields" )
}
}
You can use bytes.Buffer directly for simple cases, or CaptureBuffer for its additional helper methods like Contains(), Lines(), etc.
Source Code Reference
CaptureBuffer implementation: testing.go:9-134
View Full Implementation See the complete CaptureBuffer source code
MockLogger For structured entry inspection
Testing Overview Testing best practices