Designing Testable Go Business Code: Pure Functions, Dependency Extraction, and Best Practices
This article explains why unit testing is often seen as burdensome, then provides practical Go techniques—using pure functions, extracting dependencies, object‑oriented design, function variableization, and avoiding init—to write business code that is easy to test and maintain.
In long‑running projects, writing unit tests has become a superficial consensus: everyone says it’s good, but the extra effort makes many view it as a heavy burden.
The article uses Go as an example to show how to design business code that is easy to test.
01 Put the Elephant in the Refrigerator – a joke illustrating that testing becomes simple when inputs are well defined and outputs are predictable.
Two straightforward steps for testable code are setting all input values and asserting expected outputs, but real‑world code often hides dependencies making tests hard.
02 Pure Functions – a function that always returns the same result for the same inputs, has no side effects, and depends on no external state. Pure functions are easy to test, especially with table‑driven tests.
func Add(a, b int) int { return a+b }
func getRedisUserInfoKey(uid int64) string { return fmt.Sprintf("uinfo:%d", uid) }
func sortByAgeAsc(userList []User) []User { /* sorting logic */ }
func ParseInt(s string) (int64, error) { /* ... */ }Non‑pure examples like func NHoursLater(n int64) time.Time { return time.Now().Add(time.Duration(n) * time.Hour) } or database queries illustrate hidden dependencies.
03 Extract Dependencies – pass all dependencies as parameters, e.g., func GetUserInfoByID(uid int64, db Queryer) (*UserInfo, error) , allowing mocks in tests.
04 Object‑Oriented Approach – encapsulate many dependencies in a struct and implement methods, reducing the need for long parameter lists.
type orderCreator struct {
checker IdemChecker
orderSystem OrderSystemSDK
kafka KafkaPusher
redis Redis.Client
}
func (self *orderCreator) NewOrder(user UserInfo, order OrderInfo) error { /* ... */ }05 Function Variableization – store function pointers in variables to replace static calls during testing.
var infoContextf = log.InfoContextf
func add(ctx context.Context, a, b int) int {
c := a + b
infoContextf(ctx, "a+b=%d", c)
return c
}By swapping infoContextf with a no‑op implementation in tests, logging side effects are eliminated.
06 Avoid init Functions – init runs before main and tests, causing hidden side effects and order‑dependent bugs; prefer explicit initialization functions.
07 Final Thoughts – keep the two guiding principles in mind: clearly identify all function dependencies and extract them so they can be controlled from outside, enabling reliable unit testing with high coverage.
Additional resources include go‑sqlmock for mocking MySQL and testify/mock for creating generic mock objects.
DevOps
Share premium content and events on trends, applications, and practices in development efficiency, AI and related technologies. The IDCF International DevOps Coach Federation trains end‑to‑end development‑efficiency talent, linking high‑performance organizations and individuals to achieve excellence.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.