Interfaces, Fonctions et Modules en Go : Structurer son code pour le TDD sans le complexifier
Posté le 15 décembre 2025 • 5 min de lecture • 927 motsComment utiliser les interfaces, les fonctions et les modules Go dans une approche Test-Driven Development ? Comment mocker, où mettre les interfaces? Comment isoler un package à fonctions ?Comment éviter la complexité inutile ?

Go est un langage minimaliste, mais cette simplicité peut donner l’impression qu’il “manque quelque chose” lorsqu’on aborde des architectures plus poussées, du Test-Driven Development ou la nécessité de mocker certaines dépendances. Très vite, les développeurs venant de Java, C# ou Python se posent les mêmes questions :
Contrairement à Java ou C#, Go ne repose pas sur l’héritage, les classes ou les gros frameworks d’injection de dépendances.
En Go :
La philosophie Go se résume en une phrase :
“Accept interfaces, return structs.”
C’est un langage où l’on introduit une abstraction uniquement lorsqu’elle devient utile et pas avant cela.
Imaginons deux packages :
foo/
foo.goet un autre
bar/
bar.gobar consomme foo.
Dans le package consommateur (bar), pas dans foo.
Pourquoi ?
bar
// package bar
type Fooer interface {
Do(ctx context.Context, input string) (string, error)
}
type Service struct {
foo Fooer
}
func NewService(foo Fooer) *Service {
return &Service{foo: foo}
}Production :
f := foo.NewClient(...)
svc := bar.NewService(f)Test :
type fakeFoo struct{}
func (f *fakeFoo) Do(ctx context.Context, in string) (string, error) {
return "OK", nil
}
svc := bar.NewService(&fakeFoo{})Supposons :
// package foo
func Compute(ctx context.Context, x int) (int, error) {
...
}Tu veux isoler Compute dans bar.
Tu as deux modèles idiomatiques.
// package bar
type ComputeFunc func(ctx context.Context, x int) (int, error)
type Service struct {
compute ComputeFunc
}
func NewService() *Service {
return &Service{
compute: foo.Compute,
}
}
func NewServiceWithCompute(f ComputeFunc) *Service {
return &Service{
compute: f,
}
}Test :
svc := bar.NewServiceWithCompute(func(ctx context.Context, x int) (int, error) {
return 42, nil
})// package foo
type Computer struct{}
func NewComputer() *Computer { return &Computer{} }
func (c *Computer) Compute(ctx context.Context, x int) (int, error) {
return Compute(ctx, x)
}Et dans bar :
type Computer interface {
Compute(ctx context.Context, x int) (int, error)
}| Critère | Interface | Function Injection |
|---|---|---|
| Quand l’utiliser | Comportement complexe | Dépendance simple |
| Nombre de méthodes | Plusieurs | 1 |
| Facilité de test | Très bonne | Excellente |
| Boilerplate | Moyen | Très faible |
| Couplage | Faible | Légèrement plus fort |
| Refactoring | Très sûr | Peut casser silencieusement |
| Style Go | Très idiomatique | Parfaitement idiomatique |
Conclusion :
Si bar définit une interface Fooer, tu peux garantir la compatibilité via :
var _ bar.Fooer = (*foo.Client)(nil)Si une méthode manque ou change → la compilation échoue.
Ce pattern est standard dans tous les gros projets Go.
Voici un workflow simple, réaliste et efficace :
Commence avec des fonctions simples
Pas besoin de premature abstraction.
Observe via les tests quand l’abstraction devient nécessaire
Définis l’interface dans le consommateur
Injecte les dépendances (struct, interface ou fonction)
Utilise un fake minimal pour les tests
Vérifie la compatibilité avec :
var _ Interface = (*Impl)(nil)Refactorise quand les tests deviennent difficiles.
Go encourage un code clair, simple, sans sur-ingénierie.
Très souvent, les équipes Go isolent :
time.Now → now func() time.Timerand.Int → randFunc func() inthttp.Client.Do → Doer func(*http.Request) (*http.Response, error)os.ReadFile → readFile func(string) ([]byte, error)Pourquoi ?
Parce que c’est simple, flexible, testable, rapide — et parfaitement Go.
Tu te demandais si les frameworks de mocks deviennent inutiles :
Go encourage la simplicité, et cela se reflète dans la manière de structurer le code pour le TDD :
En appliquant ces principes, ton code Go restera :