Québec, Canada

403-1381 1re Avenue

+1 581.849.27.96

bdgouthiere@gmail.com

Interfaces et mocking en Go : pourquoi tu n'as (presque) pas besoin de framework

Ou : Comment l’IA mocke l’implémentation interne et appelle ça un test

Tu écris un service de paiement en Go. Il prend un panier, calcule le total, débite la carte via Stripe, écrit la commande dans Postgres, envoie un email de confirmation. Cinq dépendances. Tu demandes à l’IA de générer les tests. Elle te rend cinquante lignes propres avec des mocks pour tout : mockStripe, mockDB, mockEmailer, mockLogger, mockClock. Tu lances. C’est vert. Tu as 94 % de coverage. Tu commits.

Trois semaines plus tard, tu refactors ta fonction Charge pour batcher les insertions Postgres en une seule transaction au lieu de deux requêtes. Le comportement externe est strictement identique : même panier en entrée, même commande en sortie, même email envoyé. Tu lances les tests. Quarante-deux tests cassent. Aucun bug n’a été introduit. Tu viens juste de faire bouger l’implémentation interne, et ton mock — qui spécifiait que db.Exec devait être appelé exactement deux fois, dans cet ordre, avec ces arguments — vient d’exploser.

L’IA savait écrire mock.On(...).Return(...). Elle ne savait pas quoi mocker.

Dans les quatre articles précédents de la série, on a posé les fondations : pourquoi le testing est ta vraie valeur ajoutée en 2026, comment écrire des table-driven tests qui ne mentent pas, comment exploiter t.Run, t.Parallel et t.Cleanup, et comment tester des handlers HTTP avec httptest sans jamais lancer de serveur. À chaque fois, on a fait semblant que les dépendances externes — base de données, services tiers, horloge — étaient soit absentes, soit remplacées par une « fake database écrite à la main ». Aujourd’hui, on regarde ce que cette fake database voulait dire vraiment. On parle d’interfaces. On parle de mocks. Et on parle du piège qui rend tes tests verts pendant que ton code est cassé.


Mocker en Go : interfaces, contrats, et le piège du framework magique

La philosophie : Go n’a pas de mock parce que Go a des interfaces

Si tu viens de Java, Python ou JavaScript, ta première réaction en arrivant en Go est de chercher l’équivalent de Mockito, unittest.mock, ou Jest. Tu cherches le framework qui prend une classe, te fabrique magiquement un proxy, intercepte tous les appels et te laisse écrire when(repo.findById(1)).thenReturn(user). Tu ne le trouves pas. Tu paniques. Tu installes Testify. Tu installes GoMock. Tu écris du code qui ressemble à du Java en Go.

Stop. Respire. Personne ne t’a oublié. L’absence de framework « automagique » en Go n’est pas un manque, c’est une décision de design. Go a interface, et les interfaces de Go ont une particularité que les autres langages n’ont pas : elles sont implicites. Tu n’as jamais besoin d’écrire class FakeUserStore implements UserStore. Si ta struct a les bonnes méthodes avec les bonnes signatures, elle satisfait l’interface, point. Le compilateur fait la vérification. Tu n’as ni à le déclarer, ni à le demander.

Cette propriété a une conséquence directe sur le testing : n’importe quelle struct que tu écris peut servir de mock pour n’importe quelle interface, à condition d’avoir les bonnes méthodes. Tu n’as pas besoin d’un framework qui génère un proxy au runtime. Tu écris une struct, tu lui donnes les méthodes, tu la passes à la place de la vraie. Compilateur content. Tests verts. Zéro dépendance.

C’est la moitié du proverbe Go le plus utile que personne n’a écrit officiellement1 : « accept interfaces, return structs ». Tes fonctions et tes constructeurs prennent des interfaces en paramètre, et retournent des types concrets. Ce qui veut dire que partout où ta fonction prend une dépendance, elle ne sait pas — et ne veut pas savoir — si c’est la vraie ou un fake. C’est l’application directe de la loi de Postel2 : sois libéral dans ce que tu acceptes, sois strict dans ce que tu produis. Et c’est ce qui te donne le mocking gratuitement.

L’IA, elle, n’a pas grandi avec cette idée. Elle a été entraînée sur du Java, du Python et du TypeScript, où le mocking est un acte technique compliqué qui nécessite un framework. Quand tu lui demandes de tester une fonction Go, son réflexe est d’importer Testify, de générer des mocks à la chaîne, et de les configurer en six lignes par appel. Elle peut le faire — Testify et GoMock sont d’excellents outils. Mais elle te fait sauter l’étape précédente, celle où tu te demandes : est-ce que j’ai vraiment besoin d’un framework, ou est-ce que quinze lignes de Go suffisent ?


Le mock manuel : quinze lignes, zéro dépendance

Prenons un service simple. Un OrderService qui crée des commandes en lisant et écrivant dans un store quelconque :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Order struct {
    ID    int
    Total int
    Items []string
}

type OrderStore interface {
    Save(o Order) (int, error)
    FindByID(id int) (Order, error)
}

type OrderService struct {
    store OrderStore
}

func NewOrderService(s OrderStore) *OrderService {
    return &OrderService{store: s}
}

func (s *OrderService) Place(items []string, total int) (int, error) {
    if total <= 0 {
        return 0, errors.New("total must be positive")
    }
    return s.store.Save(Order{Items: items, Total: total})
}

OrderStore est une interface avec deux méthodes. En prod, tu as une implémentation PostgresOrderStore. Pour les tests, tu veux une version qui ne touche pas à Postgres. Voici la version manuelle complète :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type fakeOrderStore struct {
    saved   []Order
    nextID  int
    saveErr error
}

func (f *fakeOrderStore) Save(o Order) (int, error) {
    if f.saveErr != nil {
        return 0, f.saveErr
    }
    f.nextID++
    o.ID = f.nextID
    f.saved = append(f.saved, o)
    return f.nextID, nil
}

func (f *fakeOrderStore) FindByID(id int) (Order, error) {
    for _, o := range f.saved {
        if o.ID == id {
            return o, nil
        }
    }
    return Order{}, errors.New("not found")
}

C’est tout. Quinze lignes. Aucune dépendance. Le compilateur garantit que *fakeOrderStore satisfait OrderStore, parce qu’il a Save et FindByID avec les bonnes signatures. Tu peux maintenant l’utiliser dans n’importe quel test :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func TestPlace_Success(t *testing.T) {
    store := &fakeOrderStore{}
    svc := NewOrderService(store)

    id, err := svc.Place([]string{"widget"}, 1500)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if id != 1 {
        t.Errorf("id: got %d, want 1", id)
    }
    if len(store.saved) != 1 {
        t.Errorf("saved orders: got %d, want 1", len(store.saved))
    }
}

func TestPlace_StoreFails(t *testing.T) {
    store := &fakeOrderStore{saveErr: errors.New("db down")}
    svc := NewOrderService(store)

    _, err := svc.Place([]string{"widget"}, 1500)
    if err == nil {
        t.Fatal("expected error, got nil")
    }
}

Pas de mock.On(...).Return(...). Pas de EXPECT().Save(gomock.Any()).Times(1). Tu construis l’état initial du fake, tu l’injectes, tu vérifies l’état final. C’est une struct Go ordinaire. Tu peux la lire d’une traite, sans connaître Testify, sans connaître GoMock, sans connaître quoi que ce soit d’autre que le langage Go lui-même3.

Et il y a un truc important ici : ce test ne vérifie pas que store.Save a été appelé exactement une fois avec ces arguments précis. Il vérifie que le résultat observable — l’ID retourné, le contenu du store après l’opération — est correct. Si tu refactors Place pour batcher les inserts, ou pour ajouter une validation supplémentaire, ou pour appeler Save deux fois pour des raisons internes, ce test ne casse pas. Il casse seulement si le comportement public change. C’est exactement ce que tu veux d’un test.

Quand est-ce que cette approche commence à coûter cher ? Quand ton interface a vingt méthodes au lieu de deux. Quand tu as quinze interfaces différentes à mocker. Quand tu as besoin de vérifier l’ordre exact des appels (rare, mais ça arrive — typiquement pour des transactions). À ce moment-là, tu sors les outils.


Testify/mock : quand le manuel commence à coûter cher

Testify est la suite de testing tierce la plus utilisée en Go, par une marge énorme. Elle fait trois choses : des assertions plus expressives (assert et require), des suites de tests structurées (suite.Suite), et un système de mock (mock.Mock). On va voir les deux premières dans l’article suivant. Aujourd’hui, on s’intéresse au troisième.

Le pattern Testify pour notre OrderStore :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import "github.com/stretchr/testify/mock"

type MockOrderStore struct {
    mock.Mock
}

func (m *MockOrderStore) Save(o Order) (int, error) {
    args := m.Called(o)
    return args.Int(0), args.Error(1)
}

func (m *MockOrderStore) FindByID(id int) (Order, error) {
    args := m.Called(id)
    return args.Get(0).(Order), args.Error(1)
}

Ta struct embarque mock.Mock. Chaque méthode appelle m.Called(args...) pour enregistrer l’appel et récupérer la valeur de retour configurée par le test. Tu écris ça une fois. Ensuite, ton test ressemble à :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func TestPlace_TestifyStyle(t *testing.T) {
    store := new(MockOrderStore)
    store.On("Save", mock.Anything).Return(42, nil)

    svc := NewOrderService(store)
    id, err := svc.Place([]string{"widget"}, 1500)

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if id != 42 {
        t.Errorf("id: got %d, want 42", id)
    }
    store.AssertExpectations(t)
}

C’est plus court qu’un fake manuel ? À peine. Le code du mock lui-même est plus court (pas besoin de réimplémenter une fake DB en mémoire), mais chaque test paie le prix de la configuration explicite. Là où Testify gagne vraiment, c’est quand tu as besoin de valeurs de retour différentes selon les arguments :

1
2
store.On("FindByID", 1).Return(Order{ID: 1, Total: 100}, nil)
store.On("FindByID", 2).Return(Order{}, errors.New("not found"))

Ou quand tu veux vérifier que le service a bien appelé la dépendance avec les bons arguments via AssertExpectations.

Mais Testify a un piège qui devient grave très vite : mock.Anything. C’est pratique. C’est trop pratique. Les tests générés par l’IA en sont saturés. Tu te retrouves avec des suites entières où chaque mock dit « peu importe ce qu’on me passe, je retourne ça ». À ce moment-là, tu n’as plus un test du comportement de ton service, tu as un test que ton service appelle quelque chose, sans jamais vérifier quoi. C’est le moment de revenir au fake manuel — un fake qui fait quelque chose avec ses arguments est plus précis qu’un mock qui les ignore.


GoMock (uber-go/mock) : la génération automatique

Si tes interfaces ont vingt méthodes et que tu en as quinze, écrire les mocks à la main ou avec Testify devient un boulot à plein temps. C’est là que GoMock prend tout son sens : tu n’écris pas le mock, tu le génères depuis l’interface.

Note importante de 2026 : le repo historique github.com/golang/mock n’est plus maintenu par Google. Le fork officiel maintenu est go.uber.org/mock, repris par Uber, et c’est celui qu’il faut utiliser aujourd’hui4. Si tu vois encore golang/mock dans un projet, c’est probablement le moment de migrer — l’API est quasiment identique.

Tu installes l’outil :

1
go install go.uber.org/mock/mockgen@latest

Puis tu génères ton mock depuis l’interface :

1
mockgen -source=order_store.go -destination=mocks/order_store_mock.go -package=mocks

Et tu l’utilises dans ton test :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import (
    "testing"
    "go.uber.org/mock/gomock"

    "yourapp/mocks"
)

func TestPlace_GoMockStyle(t *testing.T) {
    ctrl := gomock.NewController(t)
    store := mocks.NewMockOrderStore(ctrl)

    store.EXPECT().
        Save(gomock.Any()).
        Return(42, nil).
        Times(1)

    svc := NewOrderService(store)
    id, err := svc.Place([]string{"widget"}, 1500)

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if id != 42 {
        t.Errorf("id: got %d, want 42", id)
    }
}

gomock.NewController(t) est lié au *testing.T : à la fin du test, le contrôleur vérifie automatiquement que toutes les attentes (EXPECT) ont été satisfaites. Pas besoin d’appeler manuellement quelque chose comme AssertExpectations. Tu peux composer des attentes ordonnées avec gomock.InOrder(...), exiger un nombre exact d’appels avec .Times(n), ou matcher les arguments avec gomock.Eq, gomock.Any, ou un matcher custom.

GoMock est puissant. Il scale aux gros projets. Il est typé statiquement (contrairement à Testify, où tu fais beaucoup de args.Get(0).(MyType)). Le coût, c’est qu’il génère du code, ce qui ajoute une étape à ton build et un dossier mocks/ qu’il faut commit ou regénérer en CI.

Junior Jules : J’ai mocké tout le projet avec GoMock. J’ai 89 % de coverage. C’est bien, non ?

Senior Sam : Tu as mocké le time.Time ?

Junior Jules : Oui, on a une Clock interface partout, c’est mockable.

Senior Sam : Et os.File ?

Junior Jules : Non, os.File est concret, j’ai dû créer un wrapper FileSystem avec dix méthodes pour le mocker.

Senior Sam : Combien de bugs t’as trouvé en faisant ça ?

Junior Jules : …zéro. Mais c’est testable maintenant.

Senior Sam : Tu as ajouté trois cents lignes de code de production pour pouvoir mocker un truc qui n’avait jamais bugué. Le coverage a monté de quatre points. Tu as introduit deux nouveaux bugs en wrappant os.File. Tu sais ce qu’on appelle ça ?

Junior Jules : Une victoire ?

Senior Sam : De la dette de mocking.


Quand NE PAS mocker

C’est la section la plus importante de l’article, et c’est celle que l’IA ignore systématiquement. Le mocking est un outil. Comme tous les outils, il a un coût. Le coût d’un mock, ce n’est pas seulement les lignes que tu écris pour le configurer — c’est le couplage que tu introduis entre ton test et l’implémentation interne du code testé. Plus tu mockes, plus tes tests deviennent fragiles aux refactorings. Plus tes tests deviennent fragiles, plus ton équipe a peur de refactorer. Plus ton équipe a peur de refactorer, plus le code pourrit. C’est une boucle.

Quelques règles qui marchent en pratique :

Ne mocke pas la stdlib. Si ta fonction utilise time.Now(), ne crée pas une interface Clock juste pour les tests. Passe le time.Time en paramètre, ou utilise time.Now directement et accepte que tes tests utilisent l’horloge réelle. Idem pour os.File, net.Conn, bytes.Buffer. Wrapper la stdlib pour la mocker, c’est ajouter de la complexité de production pour résoudre un problème de test.

Préfère un fake en mémoire à un mock pour un repository. Un inMemoryOrderStore avec une map est plus utile qu’un MockOrderStore qui doit être configuré test par test. Tu l’écris une fois, tu l’utilises partout, et il a la propriété précieuse d’être cohérent — si tu sauvegardes une commande puis tu la lis, tu retrouves la même commande. Un mock, par défaut, ne fait pas ça. Tu dois lui dire à chaque fois.

Mocke aux frontières du système, pas à l’intérieur. Stripe, c’est une frontière. Postgres, c’est une frontière. SMTP, c’est une frontière. Ces choses, oui, tu veux les mocker (ou utiliser un fake en mémoire pour Postgres, ou Testcontainers pour le vrai — sujet d’un futur article). Mais ton service interne OrderValidator qui prend un Order et retourne un error ? Pas la peine. Appelle-le directement avec un vrai Order et compare le résultat.

Si tu testes des flux critiques, fais des tests d’intégration avec les vraies dépendances. Le code de paiement, le code de migration de base, le code de sécurité — tout ça mérite des tests qui hitent du vrai (vraie DB, vrai TLS, vrai Stripe en mode test). Les mocks sont une excellente façon d’aller vite sur le 80 % des tests, mais sur les 20 % critiques, ils mentent par nature. Ils testent ce que tu crois que la dépendance fait, pas ce qu’elle fait vraiment5.


Le piège IA : mocker l’implémentation au lieu du contrat

Voici le test que l’IA va te générer pour Place si tu lui demandes naïvement :

Un mock qui sur-spécifie l'implémentation : un refactoring innocent fait tout casser
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func TestPlace_AIGenerated(t *testing.T) {
    store := new(MockOrderStore)
    store.On("Save", Order{
        Items: []string{"widget"},
        Total: 1500,
    }).Return(42, nil).Once()

    svc := NewOrderService(store)
    id, _ := svc.Place([]string{"widget"}, 1500)

    if id != 42 {
        t.Errorf("got %d, want 42", id)
    }
    store.AssertExpectations(t)
}

Ça a l’air bien. Ça passe. Ça a même l’air complet : on vérifie que Save est appelé une fois, avec exactement la bonne Order, et que le résultat est correct. Le problème, c’est que ce test spécifie exactement ce que Place fait à l’intérieur. Si tu changes l’ordre d’assignation des champs de Order, si tu ajoutes un champ CreatedAt, si tu ajoutes une étape de normalisation des items, si tu décides d’appeler Save deux fois pour des raisons de durabilité — toutes ces modifications cassent le test, alors qu’aucune ne change le comportement observable.

Le bon test, celui qui valide le contrat sans contraindre l’implémentation, c’est celui qu’on a écrit en premier avec le fake manuel. Il dit : « après avoir appelé Place(items, total), je m’attends à ce que id > 0, err == nil, et le store contienne une commande avec ces items et ce total ». Comment le service y parvient n’est pas son affaire. C’est cette absence de précision sur l’intérieur qui rend le test robuste aux refactorings.

L’heuristique de review que tu peux appliquer immédiatement, sur ton code ou sur celui de l’IA :

Si je change l’implémentation interne sans changer le comportement externe, est-ce que ce test casse ?

Si la réponse est oui, le test sur-spécifie. Soit tu le supprimes, soit tu le réécris pour qu’il vérifie une propriété observable plutôt qu’un appel précis. Ce simple filtre élimine 80 % des mauvais mocks que l’IA génère. Et il te force à réfléchir à ce que ton code est censé faire avant de réfléchir à comment il le fait.


Ce que tu peux faire maintenant

  1. Ouvre ton projet Go. Cherche mock.On( ou EXPECT(). Pour chaque mock que tu trouves, demande-toi : un fake manuel de quinze lignes ferait-il le job ? Souvent, oui.
  2. Cherche les interfaces qui n’ont qu’un seul implémentation en prod plus un mock. C’est un signal — l’interface n’existe peut-être que pour le test, et c’est OK, mais c’est aussi peut-être un wrapper inutile autour de la stdlib.
  3. Pour chaque test qui spécifie mock.On("Method", arg1, arg2).Return(...).Times(N) avec un Times exact, demande-toi si ce nombre d’appels fait partie du contrat ou de l’implémentation. Si c’est l’implémentation, retire le Times.
  4. Si tu utilises encore github.com/golang/mock, planifie ta migration vers go.uber.org/mock. L’API est quasi identique, le repo d’origine est gelé.
  5. Sur un service critique de ton projet (paiement, auth, données client), écris un test d’intégration avec la vraie dépendance, en plus de tes tests mockés. Tu vas trouver des choses.

Si ce dernier point te fait peur — « ça va prendre trois jours, on n’a pas le temps » — c’est probablement le test que tu devrais écrire en priorité.


La suite

On a maintenant une stratégie complète pour isoler les dépendances dans nos tests Go : interfaces implicites, fakes manuels par défaut, Testify ou GoMock quand le coût manuel devient prohibitif, et la règle d’or ne mocke jamais ton propre code, mocke les frontières. Mais on a esquivé la moitié de Testify : les assertions et les suites. C’est l’autre raison majeure pour laquelle 80 % des projets Go finissent par l’importer, et c’est aussi un outil double tranchant — il rend les tests plus lisibles, mais il peut aussi cacher la complexité au point que tu ne comprends plus ce que tu testes.

Dans le prochain article, on regarde Testify de plus près. La différence cruciale entre assert et require (et pourquoi te tromper coûte cher en debugging). Les suites de tests avec suite.Suite, leur intérêt et leurs pièges. Le combo Testify + table-driven tests. Et l’opinion impopulaire : quand est-ce que testing.T natif est meilleur que Testify, et pourquoi 30 % des projets Go gagneraient à supprimer Testify de leurs dépendances.


  1. « Accept interfaces, return structs » est attribué à Jack Lindamood dans un post Medium de 2016. Curiosité : le proverbe n’apparaît pas dans la liste officielle des Go proverbs, et une issue GitHub ouverte depuis 2018 demande de l’y ajouter sans réponse de Rob Pike. C’est devenu le proverbe Go le plus cité qui n’en est officiellement pas un. ↩︎

  2. La loi de Postel, formulée par Jon Postel dans la RFC 760 en 1980 pour TCP, dit : « sois conservateur dans ce que tu envoies, sois libéral dans ce que tu acceptes ». Elle a été largement remise en question depuis comme principe de design pour les protocoles réseau (elle complique l’évolution des standards), mais elle reste une excellente heuristique pour le design d’API au sein d’un même programme — où tu contrôles les deux côtés. ↩︎

  3. Cette approche du fake en mémoire est ce que Martin Fowler appelle un « fake » dans sa taxonomie des « test doubles » (dummy, fake, stub, spy, mock). Beaucoup de gens utilisent le mot « mock » pour désigner les cinq, ce qui crée de la confusion. Un vrai mock est un objet qui vérifie les interactions (les appels reçus) ; un fake est une implémentation alternative qui fonctionne mais avec des raccourcis (typiquement, en mémoire au lieu de sur disque). ↩︎

  4. Le repo github.com/golang/mock a été archivé en mode lecture seule en juin 2023. Uber a forké le projet sous go.uber.org/mock et continue de le maintenir activement, avec un suivi des nouvelles versions de Go. La doc complète du package gomock est sur pkg.go.dev/go.uber.org/mock/gomock. La migration depuis golang/mock consiste essentiellement à changer le path d’import — l’API publique est quasi-identique. ↩︎

  5. Une étude de Google sur les tests d’intégration vs unitaires défend l’inverse — privilégier le pyramide unitaire à 70 %, intégration à 20 %, end-to-end à 10 %. C’est un bon point de départ, mais la conclusion à retenir est que chaque niveau a sa raison d’être : les unitaires pour la vitesse de feedback, l’intégration pour la confiance dans les contrats. Tout mocker, c’est jouer une partie complète au niveau le plus bas et appeler ça « du test ». ↩︎