Backend Development 17 min read

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.

DevOps
DevOps
DevOps
Designing Testable Go Business Code: Pure Functions, Dependency Extraction, and Best Practices

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.

Gounit testingmockingsoftware designdependency injectiontestable codepure functions
DevOps
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.