0
0
Goprogramming~15 mins

Running tests in Go - Deep Dive

Choose your learning style9 modes available
Overview - Running tests
What is it?
Running tests in Go means checking if your code works as expected by writing special functions called tests. These tests automatically run your code with different inputs and check the results. This helps catch mistakes early and ensures your program behaves correctly. Go has built-in support for testing, making it easy to write and run tests.
Why it matters
Without running tests, bugs can hide in your code and cause problems later, sometimes in ways that are hard to find. Testing saves time and effort by catching errors early, improving code quality and confidence. It also helps when changing code, so you know nothing else breaks. In real life, this means fewer crashes and happier users.
Where it fits
Before learning to run tests, you should know basic Go programming, including functions and packages. After mastering tests, you can learn about advanced testing techniques like benchmarks, mocking, and continuous integration to automate testing in bigger projects.
Mental Model
Core Idea
Running tests means writing small programs that check if your main program works correctly by comparing expected and actual results automatically.
Think of it like...
Running tests is like having a checklist when assembling furniture: you check each step to make sure everything fits before moving on, so you don’t end up with a wobbly chair.
┌───────────────┐
│ Your Go Code  │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Test Functions│
│ (start with   │
│  TestXxx)     │
└──────┬────────┘
       │ run with `go test`
       ▼
┌───────────────┐
│ Test Runner   │
│ executes tests│
│ and reports   │
│ pass/fail     │
└───────────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding Go test files
🤔
Concept: Test files in Go have a special naming pattern and contain test functions.
In Go, test files end with _test.go. For example, if your code is in math.go, your tests go in math_test.go. This naming tells Go these files contain tests. Inside, test functions start with Test and take a *testing.T parameter. This setup lets Go find and run your tests automatically.
Result
Go recognizes test files and test functions when you run `go test`.
Knowing the naming rules helps Go find your tests without extra setup, making testing simple and consistent.
2
FoundationWriting a basic test function
🤔
Concept: A test function runs code and checks if the output matches what you expect.
A test function looks like this: func TestAdd(t *testing.T) { got := Add(2, 3) want := 5 if got != want { t.Errorf("Add(2, 3) = %d; want %d", got, want) } } Here, Add is the function being tested. If the result is wrong, t.Errorf reports an error.
Result
If Add works, the test passes silently; if not, you see an error message.
Tests automate checking results, so you don’t have to verify outputs manually every time.
3
IntermediateRunning tests with go test command
🤔Before reading on: do you think `go test` runs all tests in the current folder or only one test function? Commit to your answer.
Concept: The `go test` command runs all test functions in files ending with _test.go in the current directory.
To run tests, open your terminal in the package folder and type: go test This runs all tests and shows if they pass or fail. You can add flags like -v for detailed output or -run to run specific tests by name.
Result
You get a summary of passed and failed tests in your terminal.
Understanding how to run tests from the command line lets you quickly check your code’s health during development.
4
IntermediateUsing t.Fatal vs t.Error in tests
🤔Before reading on: do you think t.Fatal stops the test immediately or continues running? Commit to your answer.
Concept: t.Error reports an error but continues the test, while t.Fatal reports an error and stops the test immediately.
Inside a test, you can use: - t.Error("message") to log an error but keep running the test function. - t.Fatal("message") to log an error and stop the test function right away. Use t.Fatal when continuing makes no sense, like when setup fails.
Result
Tests stop early on fatal errors, saving time and avoiding confusing results.
Knowing when to stop a test early prevents misleading errors and helps focus on the root problem.
5
IntermediateTable-driven tests for multiple cases
🤔Before reading on: do you think writing separate test functions for each input is better or using a loop inside one test? Commit to your answer.
Concept: Table-driven tests use a list of inputs and expected outputs to run many test cases in one function.
Instead of writing many test functions, you create a slice of test cases: func TestAddMultiple(t *testing.T) { tests := []struct { a, b int want int }{ {1, 2, 3}, {2, 2, 4}, {10, 5, 15}, } for _, tc := range tests { got := Add(tc.a, tc.b) if got != tc.want { t.Errorf("Add(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.want) } } } This runs all cases in one test function.
Result
You get a single test that checks many inputs, making tests concise and easier to maintain.
Table-driven tests reduce repetition and make it easy to add new cases without extra functions.
6
AdvancedRunning tests with coverage reports
🤔Before reading on: do you think coverage shows which lines ran or which lines failed? Commit to your answer.
Concept: Coverage reports show which parts of your code were tested and which were not, helping find untested code.
Run tests with coverage by typing: go test -cover This shows a percentage of code tested. For detailed info, use: go test -coverprofile=coverage.out go tool cover -html=coverage.out This opens a browser showing which lines were tested (green) and which were not (red).
Result
You see exactly what code your tests cover, helping improve test quality.
Coverage helps focus testing efforts on untested code, improving reliability and reducing bugs.
7
ExpertParallel and subtests for efficient testing
🤔Before reading on: do you think tests run in parallel by default or sequentially? Commit to your answer.
Concept: Go supports running tests in parallel and organizing tests into subtests for better control and speed.
By default, tests run one after another. You can run tests in parallel by calling t.Parallel() inside a test or subtest. Subtests let you group related tests: func TestAddSubtests(t *testing.T) { cases := []struct { name string a, b int want int }{ {"small", 1, 2, 3}, {"large", 100, 200, 300}, } for _, c := range cases { c := c // capture range variable t.Run(c.name, func(t *testing.T) { t.Parallel() got := Add(c.a, c.b) if got != c.want { t.Errorf("Add(%d, %d) = %d; want %d", c.a, c.b, got, c.want) } }) } } This runs subtests in parallel, speeding up testing.
Result
Tests run faster and are better organized, especially for many cases.
Parallel and subtests improve test suite performance and clarity, essential for large projects.
Under the Hood
When you run `go test`, the Go tool compiles your test files into a temporary executable. This executable runs all functions starting with Test and passes a *testing.T object to each. The testing.T object provides methods to report success or failure. The test runner collects results and prints a summary. Tests run in the same process, sharing memory, but can be run in parallel if requested.
Why designed this way?
Go’s testing was designed to be simple and integrated, avoiding external tools. Naming conventions and the *testing.T parameter make tests easy to write and discover automatically. This design trades off some flexibility for simplicity and speed, encouraging developers to write tests as part of normal development.
┌───────────────┐
│ go test cmd   │
└──────┬────────┘
       │ compiles
       ▼
┌───────────────┐
│ Test binary   │
│ (executes)   │
└──────┬────────┘
       │ runs
       ▼
┌───────────────┐
│ Test functions│
│ TestXxx(t *testing.T)│
└──────┬────────┘
       │ calls
       ▼
┌───────────────┐
│ testing.T API │
│ reports pass/fail│
└──────┬────────┘
       │ collects
       ▼
┌───────────────┐
│ Test summary  │
│ printed to stdout│
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does `go test` run tests in all subfolders by default? Commit to yes or no.
Common Belief:Running `go test` in a folder runs tests in all subfolders recursively.
Tap to reveal reality
Reality:`go test` runs tests only in the current folder unless you specify ./... to include subfolders.
Why it matters:Assuming recursive testing can cause missed tests in subfolders, leading to false confidence.
Quick: Does a test function need to return a value to indicate pass or fail? Commit to yes or no.
Common Belief:Test functions return true or false to show if they passed or failed.
Tap to reveal reality
Reality:Test functions do not return values; they use methods on *testing.T to report failures.
Why it matters:Misunderstanding this leads to incorrect test code and confusion about how results are reported.
Quick: If a test function calls t.Error, does the test stop immediately? Commit to yes or no.
Common Belief:Calling t.Error stops the test function immediately.
Tap to reveal reality
Reality:t.Error reports an error but lets the test continue; only t.Fatal stops the test immediately.
Why it matters:Misusing t.Error and t.Fatal can cause tests to miss important checks or produce confusing output.
Quick: Does running tests with coverage guarantee all code is tested? Commit to yes or no.
Common Belief:If coverage is 100%, all code paths are tested perfectly.
Tap to reveal reality
Reality:Coverage shows which lines ran but not if all logical paths or edge cases were tested.
Why it matters:Relying solely on coverage can give a false sense of security, missing bugs in untested logic.
Expert Zone
1
Tests run in parallel share memory, so race conditions can appear if code is not thread-safe.
2
Capturing loop variables correctly in subtests is crucial to avoid subtle bugs where all subtests use the last value.
3
The testing package’s output format is designed to integrate with many tools, enabling rich reporting and automation.
When NOT to use
For very complex integration tests involving external systems, Go’s built-in testing may be insufficient; specialized frameworks or tools like Docker-based test environments or mocking libraries should be used instead.
Production Patterns
In production, tests are often run automatically on every code change using continuous integration systems. Table-driven tests and subtests are common patterns to keep tests maintainable and fast. Coverage reports guide adding tests to critical code paths.
Connections
Continuous Integration (CI)
Builds-on
Understanding how to run tests locally is essential before automating them in CI pipelines that run tests on every code change.
Concurrency in Go
Same pattern
Knowing how tests run in parallel helps understand concurrency issues and race conditions in Go programs.
Scientific Method
Builds-on
Running tests is like performing experiments to verify hypotheses, showing how programming shares principles with scientific inquiry.
Common Pitfalls
#1Test functions not named correctly so Go does not run them.
Wrong approach:func addTest(t *testing.T) { // test code }
Correct approach:func TestAdd(t *testing.T) { // test code }
Root cause:Go requires test functions to start with 'Test' to recognize and run them.
#2Using loop variable directly in subtests causing all subtests to use the last value.
Wrong approach:for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := Add(tc.a, tc.b) if got != tc.want { t.Errorf(...) } }) }
Correct approach:for _, tc := range tests { tc := tc // capture variable t.Run(tc.name, func(t *testing.T) { got := Add(tc.a, tc.b) if got != tc.want { t.Errorf(...) } }) }
Root cause:Loop variables are reused in Go, so capturing them inside the loop avoids all subtests sharing the same variable.
#3Calling t.Error when a fatal error occurs, causing misleading test continuation.
Wrong approach:if err != nil { t.Error("setup failed") } // test continues
Correct approach:if err != nil { t.Fatal("setup failed") } // test stops here
Root cause:t.Error logs an error but does not stop the test, so subsequent code may run with invalid state.
Key Takeaways
Go runs tests by finding functions named TestXxx in files ending with _test.go and executing them with a special testing object.
Tests automatically check if your code behaves as expected, saving time and preventing bugs.
The go test command runs all tests in a folder, with options to run specific tests or show coverage.
Table-driven tests and subtests help organize many test cases efficiently and can run in parallel for speed.
Understanding how tests report errors and stop execution helps write clear and reliable test code.