Utiliser les constantes en TDD avec Go
Posté le 23 décembre 2025 • 7 min de lecture • 1 310 motsComment utiliser correctement les constantes en Test-Driven Development avec Go.

Le Test-Driven Development (TDD) ne sert pas uniquement à écrire des tests.
Il sert avant tout à concevoir du logiciel.
En Go, l’usage des constantes est souvent mal compris en TDD :
Cet article propose une approche pragmatique et idiomatique Go, issue de l’expérience terrain, pour comprendre quand une constante est un bon design… et quand elle cache un problème.

Le cycle TDD est simple :
Les constantes apparaissent surtout au moment du refactor.
Elles sont rarement une optimisation : ce sont des décisions de design.
const en Go
En Go, une constante est :
const MaxRetries = 3 // non typée
const MaxRetriesInt int = 3 // typéetime.Now, variables d’environnement, etc.)Si une valeur dépend de l’environnement ou du temps, ce n’est pas une constante.
Ce sont des invariants fonctionnels :
const MaxLoginAttempts = 5Caractéristiques :
En TDD, ce sont les constantes les plus importantes.
Exemples :
const bufferSize = 32 * 1024Caractéristiques :
Les tests doivent vérifier le comportement, pas la valeur.
// production
const MaxRetries = 3
// test
const maxRetries = 3Dupliquer une constante entre le code de production et les tests est une pratique dangereuse, car elle introduit deux sources de vérité distinctes. Dans ce cas, un changement de règle métier peut laisser les tests au vert sans que le comportement réel du système soit correctement validé. Le test cesse alors de vérifier la règle métier et se contente de confirmer une implémentation figée
Le test doit utiliser la constante de production :
if !ShouldStop(MaxRetries + 1) {
t.Fatal("expected stop")
}À l’inverse, un bon design en TDD consiste à utiliser directement la constante de production dans les tests. De cette manière, le test exprime explicitement la règle métier attendue, par exemple en vérifiant qu’un dépassement de la limite déclenche bien l’arrêt du traitement. Le résultat est un test plus robuste, plus expressif, et aligné avec le langage du domaine : il valide une intention fonctionnelle, et non une simple valeur numérique.
Exemple en test :
for i := 0; i < 5; i++ {
...
}Lorsqu’une valeur mérite un nom explicite dans le code, c’est qu’elle porte un sens particulier dans le design. Dans ce cas, elle doit être soit une constante métier, soit un paramètre configurable. Prenons l’exemple d’une boucle de test utilisant la valeur 5 comme borne : avant de l’extraire dans une constante, il est essentiel de se demander si ce nombre représente une règle métier réelle. Si c’est le cas, alors cette valeur doit devenir une constante clairement nommée, capable d’exprimer l’intention fonctionnelle du code. En revanche, si ce nombre n’a aucune signification métier, il est préférable de le laisser inline afin d’éviter un bruit conceptuel inutile.
Enfin, si cette valeur est amenée à changer fréquemment selon les contextes ou les scénarios de test, elle ne doit pas être figée : elle doit être modélisée comme un paramètre ou une option, afin de préserver la flexibilité du design.
En résumé :
Si cette valeur change souvent :
Une constante qui change souvent est un mauvais signe
const Timeout = 2 * time.SecondSi tu la modifies régulièrement :
Résultat : Elle devrait être injectée, pas figée.
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
}Avantages en TDD :
var ErrTooManyRetries = errors.New("too many retries")Test:
if !errors.Is(err, ErrTooManyRetries) {
t.Fatalf("unexpected error")
}Résultat : Stable, lisible, refactor-friendly.
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")
}
}Résultat : Les constantes deviennent des invariants observables.
Les timeouts sont l’ennemi du TDD s’ils sont codés en dur.
const Timeout = 2 * time.Second
ctx, cancel := context.WithTimeout(ctx, Timeout)Valeurs sans sens métier :
{"spaces", " ", false}Conséquences :
time.Sleeptype Client struct {
timeout time.Duration
}
func NewClient(timeout time.Duration) *Client {
return &Client{timeout: timeout}
}Avec une valeur par défaut :
const DefaultTimeout = 2 * time.Second
func NewDefaultClient() *Client {
return NewClient(DefaultTimeout)
}Tests :
sleepUtilise des constantes pour :
const (
ValidUserID = "user-123"
InvalidUserID = ""
)Valeurs sans sens métier :
{"spaces", " ", false}Résultat : Lisibilité maximale, bruit minimal.
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 :
Résultat : Design robuste, testable, idiomatique Go.
En TDD, les constantes ne servent pas à “faire propre”.
Elles servent à rendre le design explicite.
En Go, bien utilisées, elles deviennent :
Et quand une constante commence à poser problème,
le TDD te donne le signal le plus précieux : ton design demande à évoluer.