Testing
Go's built-in testing vs C's manual assertions and test frameworks
Introduction
In this lesson, you'll learn about testing in Go. Coming from C, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C, you're familiar with go's built-in testing vs c's manual assertions and test frameworks.
Go has its own approach to go's built-in testing vs c's manual assertions and test frameworks, which we'll explore step by step.
The Go Way
Let's see how Go handles this concept. Here's a typical example:
// math_test.go (same directory as math.go)
package math
import (
"fmt"
"testing"
)
// Basic test — replaces C's ASSERT_EQ macro
func TestAdd(t *testing.T) {
got := Add(1, 2)
if got != 3 {
t.Errorf("Add(1,2) = %d; want 3", got)
}
}
// Table-driven test — replaces C's manual test_add()/test_divide()
func TestDivide(t *testing.T) {
tests := []struct {
a, b int
want int
wantErr bool
}{
{6, 3, 2, false},
{0, 5, 0, false},
{1, 0, 0, true}, // divide by zero
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%d/%d", tc.a, tc.b), func(t *testing.T) {
got, err := Divide(tc.a, tc.b)
if tc.wantErr {
if err == nil { t.Error("expected error") }
return
}
if err != nil { t.Fatalf("unexpected: %v", err) }
if got != tc.want { t.Errorf("got %d; want %d", got, tc.want) }
})
}
}
// go test ./... — run all tests
// go test -v ./... — verbose output
// go test -run TestAdd — run specific testComparing to C
Here's how you might have written similar code in C:
#include <stdio.h>
#include <assert.h>
#include <string.h>
int add(int a, int b) { return a + b; }
int divide(int a, int b) { return b == 0 ? -1 : a / b; }
// Manual test function
int tests_run = 0, tests_passed = 0;
#define ASSERT_EQ(actual, expected) do { \
tests_run++; \
if ((actual) != (expected)) { \
printf("FAIL: %s:%d expected %d got %d\n", \
__FILE__, __LINE__, (expected), (actual)); \
} else { \
tests_passed++; \
} \
} while(0)
void test_add(void) {
ASSERT_EQ(add(1, 2), 3);
ASSERT_EQ(add(-1, 1), 0);
}
void test_divide(void) {
ASSERT_EQ(divide(6, 3), 2);
ASSERT_EQ(divide(1, 0), -1); // error sentinel
}
int main(void) {
test_add();
test_divide();
printf("%d/%d tests passed\n", tests_passed, tests_run);
return tests_passed == tests_run ? 0 : 1;
}You may be used to different syntax or behavior.
Go has built-in test runner — no separate framework or Makefile target needed
You may be used to different syntax or behavior.
t.Errorf formats failure messages — replaces C's printf + test counter
You may be used to different syntax or behavior.
t.Run creates named sub-tests; -run TestAdd/6/3 filters to specific cases
You may be used to different syntax or behavior.
Go errors are return values; test with != nil (not error codes like C's -1)
You may be used to different syntax or behavior.
Test files (_test.go) are excluded from production builds automatically
Step-by-Step Breakdown
1. Test File and Function
Name test files _test.go. Test functions are TestXxx(t *testing.T). 'go test' discovers and runs them automatically.
// C: manual main() calling test functions
int main(void) { test_add(); test_divide(); }// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
if Add(1,2) != 3 { t.Fail() }
}
// go test ./... — automatic discovery2. Failure Messages
t.Errorf formats the failure message — much better than C's printf macro. t.Fatal stops the test; t.Error continues.
#define ASSERT_EQ(a,e) if((a)!=(e)) printf("FAIL: got %d\n",(a))if got := Add(1, 2); got != 3 {
t.Errorf("Add(1,2) = %d; want 3", got)
}3. Table-Driven Tests
Table-driven tests replace C's manual test_xxx() functions. Each case is a struct; t.Run gives each a name.
void test_divide() {
assert(divide(6,3)==2);
assert(divide(1,0)==-1);
}tests:=[]struct{a,b,want int}{{6,3,2},{0,5,0}}
for _,tc:=range tests {
if Divide(tc.a,tc.b)!=tc.want { t.Fail() }
}4. Testable Code Design
Go makes code testable through interfaces. Accept interfaces in functions so tests can pass mock implementations.
// C: function pointers for testing (complex)
typedef int (*ReadFn)(char*, size_t);
void process(ReadFn read) { ... }type Reader interface { Read(p []byte) (int, error) }
func Process(r Reader) error { ... }
// Real: Process(os.Stdin)
// Test: Process(strings.NewReader("test data"))Common Mistakes
When coming from C, developers often make these mistakes:
- Go has built-in test runner — no separate framework or Makefile target needed
- t.Errorf formats failure messages — replaces C's printf + test counter
- t.Run creates named sub-tests; -run TestAdd/6/3 filters to specific cases
Key Takeaways
- _test.go files; TestXxx(t *testing.T); 'go test ./...' — no Makefile or framework
- t.Errorf for failure messages; t.Fatal to stop; t.Error to continue
- Table-driven tests (struct slice + t.Run) replace manual C test functions
- Design for testability: accept interfaces, not concrete types