Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
176: Subtests with t.Run
Imagine you're a quality assurance inspector at a car factory. Your job is to "Verify the Electrical System." That sounds like one single task, but in reality, it's a collection of smaller checks: Do the headlights work? Do the blinkers flash? Does the radio turn on? If the blinkers are broken, you don't just throw your hands up and mark the entire car as "Electrical Failure" and walk away. You mark the blinkers as failed, but you still check the radio and the headlights to see exactly what's broken.
In Go, t.Run is how we do exactly that. It lets us take one big test function—the "Electrical System" check—and carve it into smaller, named subtests. This is a huge upgrade from just looping through a slice of test cases and calling t.Errorf, because it gives each case its own identity and its own lifecycle.
Stopping the "All-or-Nothing" Failure
I've seen a lot of developers write table-driven tests using a simple for loop. It works fine until you have twenty cases and the third one fails. Suddenly, the test output is a wall of text, and it's a pain to figure out exactly which input caused the crash. Worse, if you use t.Fatalf inside a loop, the first failure kills the entire test suite for that function. You lose visibility into whether the other nineteen cases would have passed.
When you wrap your test logic in t.Run, Go treats each subtest as its own entity. If one subtest fails, the others keep running. Plus, the output tells you exactly which subtest failed by printing the name you gave it.
The Table-Driven Pattern with Subtests
Let's look at a real scenario. Suppose you're writing a function that validates passwords. It needs to check for length, numbers, and special characters. Instead of writing TestPasswordLength, TestPasswordNumbers, and so on, we group them.
func TestValidatePassword(t *testing.T) {
tests := []struct {
name string
password string
want bool
}{
{"TooShort", "123!", false},
{"NoNumber", "Password!", false},
{"NoSpecial", "Password123", false},
{"Valid", "StrongPass123!", true},
}
for _, tc := range tests {
// We use t.Run to create a named subtest for each case
t.Run(tc.name, func(t *testing.T) {
got := ValidatePassword(tc.password)
if got != tc.want {
t.Errorf("ValidatePassword(%q) = %v; want %v", tc.password, got, tc.want)
}
})
}
}
Notice how I'm passing tc.name as the first argument. If the "NoNumber" case fails, Go will report the failure as TestValidatePassword/NoNumber. It makes your logs infinitely more readable.
Picking and Choosing Your Tests
One of my favorite "hidden" benefits of t.Run is how it interacts with the command line. When you have a massive test suite, you don't always want to run everything. Because subtests have names, you can use the -run flag with a regular expression to execute only a specific subtest.
If you only want to debug the "NoNumber" case from the example above, you can run:
go test -v -run TestValidatePassword/NoNumber
This saves you a ton of time. You aren't waiting for the entire suite to finish just to see if your fix for one specific edge case worked. I use this constantly when I'm hunting down a tricky bug in a complex data pipeline.
A Quick Note on Closures
One thing to watch out for: in older versions of Go (before 1.22), using the loop variable tc inside the t.Run closure was a common trap. Because the loop variable was reused, all your subtests might have ended up running the final test case. In modern Go, this is handled for you, but if you're working on a legacy codebase, you might see people manually re-declaring the variable inside the loop (e.g., tc := tc). Just keep that in the back of your mind if you see some weird shadowing happening.
📋 Practical Task
Refactoring the Shipping Calculator Tests
You have been handed a CalculateShipping function that determines costs based on weight and destination. The current test suite is a single loop that is hard to debug. Your task is to refactor the existing test into a table-driven test using t.Run.
The Function to Test:
func CalculateShipping(weight float64, destination string) float64 {
if destination == "Local" {
return weight * 0.5
}
if destination == "International" {
return weight * 2.0
}
return weight * 1.0 // Default/National
}
Your Requirements:
- Create a
TestCalculateShippingfunction. - Define a slice of anonymous structs containing: a
name(string),weight(float64),destination(string), andwant(float64). - Include at least four test cases: one for Local, one for International, one for National, and one for a zero-weight edge case.
- Wrap the logic inside the loop with
t.Run, using thenamefield from your struct. - Ensure the test fails with a descriptive message if the calculated shipping doesn't match the expected value.
There are no comments for now.