Backend Development 16 min read

How to Write Testable Business Code in Go: From Pure Functions to Dependency Injection

This article explains why unit testing is often seen as burdensome in long‑running Go projects and provides practical techniques—such as using pure functions, extracting dependencies, object‑oriented design, and function variableization—to make business code easy to test and maintain.

FunTester
FunTester
FunTester
How to Write Testable Business Code in Go: From Pure Functions to Dependency Injection

In long‑term Go projects, writing unit tests is frequently regarded as a heavy burden because developers must set up environments, mock external services like Redis and MySQL, and control hidden dependencies.

The article demonstrates that by consciously designing data structures and function interfaces, code can become naturally testable without complex tricks.

Pure Functions : Functions that return the same result for the same inputs, have no side effects, and depend on no external state are easy to test. Examples include simple arithmetic functions and utility helpers.

func Add(a, b int) int {
    return a + b
}

func getRedisUserInfoKey(uid int64) string {
    return fmt.Sprintf("uinfo:%d", uid)
}

Table‑driven tests can be used to verify pure functions with clear input‑output pairs.

Extracting Dependencies : Pass external resources (e.g., database connections) as parameters or interfaces rather than accessing them directly inside the function. This makes the dependencies controllable in tests.

type Queryer interface {
    Query(string, args ...interface{}) (*sql.Rows, error)
}

func GetUserInfoByID(uid int64, db Queryer) (*UserInfo, error) {
    val, err := db.Query("select * from t_user where id=? limit 1", uid)
    if err != nil {
        return nil, err
    }
    return UnmarshalUserInfo(val)
}

Mock implementations of Queryer can be supplied in tests to control returned data.

Object‑Oriented Approach : Encapsulate many dependencies inside a struct and implement business logic as methods. This reduces the number of parameters while keeping dependencies injectable.

type orderCreator struct {
    checker      IdemChecker
    orderSystem OrderSystemSDK
    kafka       KafkaPusher
    redis       Redis.Client
}

func (oc *orderCreator) NewOrder(user UserInfo, order OrderInfo) error {
    // business logic using oc.checker, oc.orderSystem, etc.
    return nil
}

Function Variableization (Stubbing) : Store calls to static functions (e.g., logging, time.Now) in package‑level variables that can be swapped out during tests.

var (
    infoContextf = log.InfoContextf
    getNow       = time.Now
)

func add(ctx context.Context, a, b int) int {
    c := a + b
    infoContextf(ctx, "a+b=%d", c)
    return c
}

Tests replace infoContextf or getNow with mock functions to eliminate side effects.

The article concludes with two guiding principles: clearly identify all function dependencies and make them controllable from the outside, and three concrete methods—parameter injection, struct‑based objects, and function variableization—to achieve easily testable code.

gounit testingdependency injectionclean codepure functions
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

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.