0
0
Goprogramming~15 mins

Writing basic test functions in Go - Deep Dive

Choose your learning style9 modes available
Overview - Writing basic test functions
What is it?
Writing basic test functions in Go means creating small pieces of code that check if other parts of your program work correctly. These test functions run automatically and tell you if something is broken. They help you catch mistakes early and make your code more reliable. Tests are written in special files and use Go's built-in testing tools.
Why it matters
Without test functions, bugs can hide in your code and cause problems later, sometimes in ways you can't easily see. Writing tests helps you find errors quickly and fix them before they affect users. It also makes changing your code safer because you can check if everything still works after updates. This saves time and frustration in the long run.
Where it fits
Before writing test functions, you should know how to write basic Go programs and functions. After learning tests, you can explore more advanced testing techniques like table-driven tests, benchmarks, and using testing libraries. Testing is a key skill that fits into writing clean, maintainable Go code.
Mental Model
Core Idea
A test function is a small program that runs your code with known inputs and checks if the outputs match what you expect.
Think of it like...
Testing your code is like checking your homework answers with a solution sheet before handing it in to catch mistakes early.
┌───────────────┐
│ Test Function │
├───────────────┤
│ 1. Setup      │
│ 2. Run Code   │
│ 3. Check Out  │
│ 4. Report     │
└───────────────┘
Build-Up - 6 Steps
1
FoundationUnderstanding Go test files
🤔
Concept: Test functions live in files ending with _test.go and use the testing package.
In Go, tests are placed in files named like example_test.go. These files are separate from your main code. To write tests, you import the "testing" package. The Go tool recognizes these files and runs the test functions inside them when you run 'go test'.
Result
You have a special file ready to hold your test functions, and Go knows to run them.
Knowing the special naming and package rules is the first step to making Go run your tests automatically.
2
FoundationWriting a simple test function
🤔
Concept: Test functions start with Test, take *testing.T, and use t.Error or t.Fatal to report failures.
A basic test function looks like this: func TestAdd(t *testing.T) { result := Add(2, 3) if result != 5 { t.Error("Expected 5, got", result) } } Here, Add is the function being tested. If the result is wrong, t.Error reports it.
Result
When you run 'go test', this function runs and tells you if Add works as expected.
Understanding the test function signature and how to report errors lets you check your code automatically.
3
IntermediateUsing t.Fatal vs t.Error
🤔Before reading on: do you think t.Fatal stops the test immediately or continues after reporting an error? Commit to your answer.
Concept: t.Error reports an error but continues running the test; t.Fatal reports and stops the test immediately.
Inside a test, t.Error logs a failure but lets the test keep running. t.Fatal logs the failure and stops the test right there. Use t.Fatal when continuing makes no sense, like if setup failed.
Result
Tests can either continue after a problem or stop immediately, depending on which method you use.
Knowing when to stop a test early prevents confusing errors and wasted time in longer tests.
4
IntermediateChecking multiple cases with subtests
🤔Before reading on: do you think subtests run all cases even if one fails, or stop at the first failure? Commit to your answer.
Concept: Subtests let you group related test cases and run them separately inside one test function.
You can use t.Run to create subtests: func TestAddMultiple(t *testing.T) { cases := []struct { name string a, b int want int }{ {"2+3", 2, 3, 5}, {"0+0", 0, 0, 0}, {"-1+1", -1, 1, 0}, } for _, c := range cases { c := c // copy to new variable t.Run(c.name, func(t *testing.T) { got := Add(c.a, c.b) if got != c.want { t.Errorf("Expected %d, got %d", c.want, got) } }) } } Each case runs as a separate subtest.
Result
You get detailed results for each case, making it easier to find which inputs fail.
Using subtests organizes your tests and helps isolate failures for clearer debugging.
5
AdvancedRunning tests with go test command
🤔Before reading on: do you think 'go test' runs tests in all packages by default or only the current one? Commit to your answer.
Concept: The 'go test' command finds and runs all test functions in the current package or specified packages.
When you run 'go test' in a folder, Go looks for *_test.go files and runs all Test* functions inside. You can add flags like -v for verbose output or -run to select specific tests. You can also test multiple packages by specifying them.
Result
You can run your tests easily from the command line and see which pass or fail.
Mastering the test command lets you integrate testing smoothly into your workflow.
6
ExpertUnderstanding test function execution order
🤔Before reading on: do you think Go runs test functions in the order they appear in code or in random order? Commit to your answer.
Concept: Go runs test functions in the order they are discovered, which is not guaranteed to match source order, and subtests run in the order coded unless parallelized.
Go's test runner discovers test functions by name and runs them, but the order is not guaranteed. Subtests run in the order they appear unless you call t.Parallel() to run them concurrently. This means tests should not depend on order or shared state.
Result
Tests run independently and in unpredictable order, encouraging better test design.
Knowing test execution order prevents flaky tests and helps design reliable, isolated tests.
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 failures and control test flow. The test runner collects results and prints a summary. Tests run in their own process, isolated from your main program.
Why designed this way?
Go's testing was designed to be simple and integrated with the language toolchain. Using naming conventions and the testing package avoids extra configuration. Running tests as compiled programs ensures they run fast and isolated, and the *testing.T interface gives a clean way to report errors and control tests.
┌───────────────┐
│ go test cmd   │
└──────┬────────┘
       │ compiles
┌──────▼────────┐
│ Test Executable│
│ ┌───────────┐ │
│ │ TestFunc1 │ │
│ │ TestFunc2 │ │
│ └───────────┘ │
└──────┬────────┘
       │ runs
┌──────▼────────┐
│ testing.T obj │
│ reports pass/ │
│ fail to runner│
└───────────────┘
Myth Busters - 3 Common Misconceptions
Quick: Does a test function need to return a value to indicate success? Commit to yes or no.
Common Belief:Test functions must return true or false to show if the test passed or failed.
Tap to reveal reality
Reality:Test functions do not return values; they use the *testing.T methods to report failures.
Why it matters:Expecting return values can confuse beginners and lead to incorrect test code that doesn't report failures properly.
Quick: Do you think tests run in the order they appear in the source code? Commit to yes or no.
Common Belief:Tests always run in the order they are written in the file.
Tap to reveal reality
Reality:Go does not guarantee test execution order; tests may run in any order.
Why it matters:Relying on order can cause flaky tests and hidden bugs when tests depend on side effects.
Quick: Can you use t.Fatal inside a goroutine spawned in a test and expect it to stop the test? Commit to yes or no.
Common Belief:Calling t.Fatal inside any goroutine stops the entire test immediately.
Tap to reveal reality
Reality:t.Fatal only stops the goroutine it is called in; if called in another goroutine, the main test continues.
Why it matters:Misusing t.Fatal in goroutines can cause tests to pass incorrectly or hide failures.
Expert Zone
1
Tests should avoid shared state or use synchronization because tests run in parallel or unpredictable order.
2
Using t.Helper() inside helper functions marks them so error reports point to the caller, improving debugging clarity.
3
Subtests can be run in parallel with t.Parallel(), but this requires careful handling of shared resources.
When NOT to use
Basic test functions are not enough for complex scenarios like mocking, integration tests, or performance benchmarks. In those cases, use testing frameworks like testify, or Go's benchmark tools.
Production Patterns
In production, tests are often organized with table-driven tests for many cases, use setup and teardown helpers, and integrate with CI pipelines that run 'go test' automatically on code changes.
Connections
Unit Testing
Basic test functions are the foundation of unit testing practice.
Understanding how to write simple test functions helps grasp the core idea of testing small units of code independently.
Continuous Integration (CI)
Automated test functions run in CI pipelines to catch errors before deployment.
Knowing how tests run locally prepares you to integrate them into automated workflows that improve software quality.
Scientific Method
Writing tests is like forming hypotheses and experiments to verify them.
Seeing tests as experiments helps appreciate the importance of repeatability and clear expected outcomes.
Common Pitfalls
#1Writing test functions that do not start with 'Test' or have wrong signature.
Wrong approach:func addTest(t *testing.T) { // test code }
Correct approach:func TestAdd(t *testing.T) { // test code }
Root cause:Go's test runner only recognizes functions starting with 'Test' and with the correct signature.
#2Using t.Error when a failure should stop the test immediately.
Wrong approach:if err != nil { t.Error("Failed") // code continues }
Correct approach:if err != nil { t.Fatal("Failed") // test stops here }
Root cause:Not understanding the difference between t.Error and t.Fatal leads to tests continuing after critical failures.
#3Sharing variables between subtests without copying them inside the loop.
Wrong approach:for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := Add(c.a, c.b) if got != c.want { t.Errorf("Expected %d, got %d", c.want, got) } }) }
Correct approach:for _, c := range cases { c := c // copy to new variable t.Run(c.name, func(t *testing.T) { got := Add(c.a, c.b) if got != c.want { t.Errorf("Expected %d, got %d", c.want, got) } }) }
Root cause:Loop variables are reused in Go, causing all subtests to see the last value unless copied.
Key Takeaways
Go test functions live in files ending with _test.go and must start with 'Test' and accept *testing.T.
Inside test functions, use t.Error to report errors and continue, or t.Fatal to stop immediately on failure.
Subtests with t.Run help organize multiple test cases and provide detailed results.
The 'go test' command automatically finds and runs all test functions in your package.
Tests run in unpredictable order and should be independent to avoid flaky results.