C
GO

C to Go

10 lessons

Progress0%
1Variables & Types2Functions3Arrays & Slices4Structs & Methods5Pointers6Concurrency7Header Files → Packages8Error Handling9Testing10Standard Library
All Mirror Courses
C
GO
Testing
MirrorLesson 9 of 10
Lesson 9

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.

Mirror Card
C
From C:

In C, you're familiar with go's built-in testing vs c's manual assertions and test frameworks.

GO
In Go:

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:

GO
Go 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 test

Comparing to C

Here's how you might have written similar code in C:

C
C (What you know)
#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;
}
Mirror Card
C
From C:

You may be used to different syntax or behavior.

GO
In Go:

Go has built-in test runner — no separate framework or Makefile target needed

Mirror Card
C
From C:

You may be used to different syntax or behavior.

GO
In Go:

t.Errorf formats failure messages — replaces C's printf + test counter

Mirror Card
C
From C:

You may be used to different syntax or behavior.

GO
In Go:

t.Run creates named sub-tests; -run TestAdd/6/3 filters to specific cases

Mirror Card
C
From C:

You may be used to different syntax or behavior.

GO
In Go:

Go errors are return values; test with != nil (not error codes like C's -1)

Mirror Card
C
From C:

You may be used to different syntax or behavior.

GO
In Go:

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
C
// C: manual main() calling test functions
int main(void) { test_add(); test_divide(); }
GO
Go
// math_test.go
package math
import "testing"

func TestAdd(t *testing.T) {
    if Add(1,2) != 3 { t.Fail() }
}
// go test ./...  — automatic discovery

2. Failure Messages

t.Errorf formats the failure message — much better than C's printf macro. t.Fatal stops the test; t.Error continues.

C
C
#define ASSERT_EQ(a,e) if((a)!=(e)) printf("FAIL: got %d\n",(a))
GO
Go
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.

C
C
void test_divide() {
    assert(divide(6,3)==2);
    assert(divide(1,0)==-1);
}
GO
Go
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
C
// C: function pointers for testing (complex)
typedef int (*ReadFn)(char*, size_t);
void process(ReadFn read) { ... }
GO
Go
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
Common Pitfall
Don't assume Go works exactly like C. While the concepts may be similar, the syntax and behavior can differ significantly.

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
Rule of Thumb
The best way to learn is by doing. Try rewriting some of your C code in Go to practice these concepts.
PreviousNext