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.
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.
FunTester
10k followers, 1k articles | completely useless
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.