Simple Enough Blog logo
  • Home 
  • Projects 
  • Tags 

  •  Language
    • English
    • Français
  1.   Blogs
  1. Home
  2. Blogs
  3. Using Constants in TDD with Go

Using Constants in TDD with Go

Posted on December 23, 2025 • 6 min read • 1,145 words
Golang   Tdd   Architecture   Tests   Devops   Thibault  
Golang   Tdd   Architecture   Tests   Devops   Thibault  
Share via
Simple Enough Blog
Link copied to clipboard

How to correctly use constants in Test-Driven Development with Go.

On this page
Best Practices, Pitfalls, and Design Signals   I. Reminder: The Role of TDD in Design   II. What const Really Means in Go   Impact on TDD   III. Two Major Categories of Constants   1. Domain (Business) Constants   2. Technical Constants   IV. The Classic Pitfall: Duplicating Constants in Tests   Anti-pattern   Good TDD Pattern   V. A Simple (and Very Reliable) Heuristic   VI. Constants vs Parameters: The Real Design Signal   Recommended Go Pattern: Default + Options   VII. Constants and Errors: A Key Go Pattern   1. Sentinel Errors (Recommended)   2. Typed Errors (More Advanced)   VIII. Critical Case: Timeouts and TDD   Example of Bad Design   Example of TDD-Friendly Design   IX. Table-Driven Tests and Constants   X. Complete Example: Clean TDD with Constants and Options   Specification   XI. Golden Rules (Keep These Handy)   Conclusion   🔗 Useful Resources  
Using Constants in TDD with Go
Photo by Thibaut Deheurles

Best Practices, Pitfalls, and Design Signals  

Test-Driven Development (TDD) is not only about writing tests.
Its primary purpose is to design software.

In Go, the use of constants is often misunderstood in TDD:

  • should they be exposed?
  • tested?
  • injected?
  • avoided?

This article proposes a pragmatic and idiomatic Go approach, based on real-world experience, to understand when a constant represents good design… and when it hides a deeper problem.


I. Reminder: The Role of TDD in Design  

TDD cycle

The TDD cycle is simple:

  1. Red – write a failing test
  2. Green – write the minimum code to make it pass
  3. Refactor – improve the design without changing behavior

Constants usually appear during the refactor phase.
They are rarely an optimization: they are design decisions.


II. What const Really Means in Go  

In Go, a constant is:

  • evaluated at compile time
  • immutable
  • sometimes untyped (until it is used)
const MaxRetries = 3        // untyped
const MaxRetriesInt int = 3 // typed

Impact on TDD  

  • Untyped constants are often more flexible in tests
  • Typed constants can help secure a public API
  • A constant cannot depend on runtime values (time.Now, environment variables, etc.)

If a value depends on time or the environment, it is not a constant.


III. Two Major Categories of Constants  

1. Domain (Business) Constants  

These are functional invariants:

  • thresholds
  • limits
  • explicit business rules
const MaxLoginAttempts = 5

Characteristics:

  • often exported
  • documented
  • used by both production code AND tests

In TDD, these are the most important constants.

2. Technical Constants  

Examples:

  • buffer size
  • chunk size
  • internal batch limits
  • performance-related values
const bufferSize = 32 * 1024

Characteristics:

  • usually private
  • likely to change
  • not directly tested

Tests should verify behavior, not values.


IV. The Classic Pitfall: Duplicating Constants in Tests  

Anti-pattern  

// production
const MaxRetries = 3

// test
const maxRetries = 3

Duplicating a constant between production code and tests is dangerous because it introduces two sources of truth.
A business rule may change while tests remain green, even though the actual behavior is no longer correctly validated. The test stops verifying the business rule and merely confirms a frozen implementation.

Good TDD Pattern  

Tests should use the production constant:

if !ShouldStop(MaxRetries + 1) {
    t.Fatal("expected stop")
}

In contrast, a good TDD design directly reuses the production constant in tests. This allows the test to explicitly express the expected business rule, for example by verifying that exceeding the limit correctly stops processing. The result is a more robust and expressive test, aligned with the domain language: it validates an intent, not a numeric value.


V. A Simple (and Very Reliable) Heuristic  

Example in a test:

for i := 0; i < 5; i++ {
    ...
}

When a value deserves an explicit name in code, it means it carries design meaning. In that case, it should be either a business constant or a configurable parameter. Using 5 as a loop boundary in a test is a good example: before extracting it into a constant, you should ask whether this number represents a real business rule. If it does, it should become a clearly named constant expressing the functional intent. If it has no business meaning, it is better left inline to avoid unnecessary conceptual noise.

Finally, if this value is expected to change frequently across contexts or test scenarios, it should not be frozen as a constant but modeled as a parameter or option, preserving design flexibility.

In summary:

  • Is 5 a business rule?
    • yes → constant
    • no → leave it inline

If this value changes often:

  • it is not a constant
  • it is a parameter (or option)

VI. Constants vs Parameters: The Real Design Signal  

A constant that changes often is a bad sign:

const Timeout = 2 * time.Second

If you modify it regularly, you will see:

  • unstable tests
  • painful refactors
  • rigid design

Result: it should be injected, not frozen.

Recommended Go Pattern: Default + Options  

const DefaultRetryLimit = 3

type Retrier struct {
    limit int
}

type Option func(*Retrier)

func WithLimit(n int) Option {
    return func(r *Retrier) { r.limit = n }
}

func NewRetrier(opts ...Option) *Retrier {
    r := &Retrier{limit: DefaultRetryLimit}
    for _, opt := range opts {
        opt(r)
    }
    return r
}

Benefits in TDD:

  • the default is testable
  • edge cases are easy to cover
  • the design remains flexible

VII. Constants and Errors: A Key Go Pattern  

1. Sentinel Errors (Recommended)  

var ErrTooManyRetries = errors.New("too many retries")

Test:

if !errors.Is(err, ErrTooManyRetries) {
    t.Fatalf("unexpected error")
}

Result: stable, readable, refactor-friendly.

2. Typed Errors (More Advanced)  

type RetryError struct {
    Limit int
}

func (e RetryError) Error() string {
    return "too many retries"
}

Test:

var re RetryError
if errors.As(err, &re) {
  if re.Limit != DefaultRetryLimit {
  t.Fatal("unexpected limit")
  }
}

Result: constants become observable invariants.


VIII. Critical Case: Timeouts and TDD  

Timeouts are enemies of TDD when hardcoded.

Example of Bad Design  

const Timeout = 2 * time.Second

ctx, cancel := context.WithTimeout(ctx, Timeout)

Values with no business meaning:

{"spaces", "   ", false}

Consequences:

  • slow tests
  • reliance on time.Sleep
  • flakiness

Example of TDD-Friendly Design  

type Client struct {
    timeout time.Duration
}

func NewClient(timeout time.Duration) *Client {
    return &Client{timeout: timeout}
}

With a default value:

const DefaultTimeout = 2 * time.Second

func NewDefaultClient() *Client {
    return NewClient(DefaultTimeout)
}

Tests:

  • short timeout
  • zero timeout
  • no sleep

IX. Table-Driven Tests and Constants  

Use constants for:

  • symbolic cases
  • boundaries
  • invariants
const (
    ValidUserID   = "user-123"
    InvalidUserID = ""
)

Values with no business meaning:

{"spaces", "   ", false}

Result: maximum readability, minimal noise.


X. Complete Example: Clean TDD with Constants and Options  

Specification  

  • rate limiter
  • default: 100 req/min
  • configurable
  • explicit error on overflow
const DefaultLimitPerMinute = 100

var ErrRateLimited = errors.New("rate limited")

type Limiter struct {
    limit int
    used  int
}

func WithLimit(n int) Option {
    return func(l *Limiter) { l.limit = n }
}

func NewLimiter(opts ...Option) *Limiter {
    l := &Limiter{limit: DefaultLimitPerMinute}
    for _, opt := range opts {
        opt(l)
    }
    return l
}

Tests:

  • one test for the default
  • one test for variation
  • no duplicated values

Result: robust, testable, idiomatic Go design.


XI. Golden Rules (Keep These Handy)  

  • A constant is a design decision
  • Business constants must be visible to tests
  • Never duplicate a production constant in a test
  • If it changes often → parameter or option
  • Timeouts and backoff → inject, never hardcode
  • Tests must speak the domain language, not numbers

Conclusion  

In TDD, constants are not about “cleaning up” code.
They exist to make design explicit.

In Go, when used correctly, they become:

  • documented invariants
  • anchor points for tests
  • clear architectural signals

And when a constant starts causing friction, TDD provides the most valuable signal:

your design needs to evolve.


🔗 Useful Resources  

  • Dave Farley – Modern TDD & Continuous Delivery (YouTube)
    Practical talks and real-world feedback on applying TDD to real systems.

  • Effective Go – Constants & Design
    Official Go documentation explaining idiomatic language choices.
    https://go.dev/doc/effective_go

 “It’s slow”, “it lags”, “it bugs”: but in the end, what does it really mean?
Interfaces, Functions and Modules in Go: Structuring Your Code for TDD Without Adding Unnecessary Complexity 
  • Best Practices, Pitfalls, and Design Signals  
  • I. Reminder: The Role of TDD in Design  
  • II. What const Really Means in Go  
  • III. Two Major Categories of Constants  
  • IV. The Classic Pitfall: Duplicating Constants in Tests  
  • V. A Simple (and Very Reliable) Heuristic  
  • VI. Constants vs Parameters: The Real Design Signal  
  • VII. Constants and Errors: A Key Go Pattern  
  • VIII. Critical Case: Timeouts and TDD  
  • IX. Table-Driven Tests and Constants  
  • X. Complete Example: Clean TDD with Constants and Options  
  • XI. Golden Rules (Keep These Handy)  
  • Conclusion  
  • 🔗 Useful Resources  
Follow us

We work with you!

   
Copyright © 2026 Simple Enough Blog All rights reserved. | Powered by Hinode.
Simple Enough Blog
Code copied to clipboard