Mise en pratique, React + Go (Gin / GORM / PostgreSQL)
Article 6 de la série “De la spécification à l’exécution”. Cinq articles plus tôt, j’ai posé la spec, les rôles, la mesure et le property-based testing. Il reste à voir à quoi ressemble le workflow sur une vraie pile.
Je prends ici un stack volontairement courant. React côté front, Go avec Gin, GORM et PostgreSQL côté back. Le cas d’usage est simple. C’est justement ce qui le rend utile. Le repo public contient un boilerplate runnable dans examples/billing-react-go/.
Je veux montrer trois choses. D’abord, comment je répartis les tests par couche. Ensuite, quels choix changent vraiment la fiabilité. Enfin, comment je peux repartir de ce squelette sans réécrire le workflow à la main.
Cartographie de la pile de tests
Je garde une pyramide de tests classique, avec une couche de property-based testing au niveau bas.
| Couche | Outil principal | Vitesse | Ce que je teste |
|---|---|---|---|
| Front, utils et formatters | Vitest | ms | Logique métier isolée |
| Front, composants React | Vitest + RTL | ms-s | Rendu et interactions |
| Front, vues avec API | Vitest + RTL + MSW | s | Composants avec données simulées |
| Front, invariants logiques | fast-check | s | Parsing, calcul, état |
| Back, fonctions pures | testing + testify | µs-ms | Helpers, calcul, validation |
| Back, handlers HTTP | httptest + Gin | ms | Routing, validation, réponses |
| Back, repository GORM | testcontainers-go + PostgreSQL | s | SQL réel, migrations, transactions |
| Back, invariants de logique | rapid | s | Propriétés sur pricing, routing, state |
| Cross-stack, contract API | Pact | s | Compatibilité front-back sans E2E complet |
| Cross-stack, E2E | Playwright | s-min | Parcours critiques |
La pyramide reste valable. Je ne cherche pas à tout mettre en bout de chaîne. Le property-based testing me permet surtout d’augmenter la force des tests en bas de pile, pas leur volume.
Trois choix structurants font la différence ici. Ils comptent plus que le nom du framework.
Choix structurant 1, testcontainers-go plutôt que sqlmock
Le piège classique, côté backend Go avec GORM, consiste à tester le repository en mockant *sql.DB avec sqlmock. C’est rapide. C’est isolé. C’est aussi fragile.
Le problème est simple. Je ne teste pas PostgreSQL. Je teste le SQL que GORM a généré, puis je fais semblant que la base se comporte comme prévu. Dès qu’une migration change, qu’une contrainte s’ajoute ou qu’une version de GORM modifie le SQL produit, le test peut continuer à passer alors que la base réelle ne suivra pas.
Je préfère testcontainers-go. Je démarre un vrai PostgreSQL dans un container, j’applique les migrations, puis je teste le repository contre la base réelle. Le coût reste raisonnable. Quelques secondes de plus par package me semblent un bon échange contre des erreurs de base de données évitées.
Pattern de base :
//go:build integration
func setupDB(t *testing.T) (*gorm.DB, func()) {
ctx := context.Background()
pg, _ := tcpostgres.Run(ctx, "postgres:16-alpine", ...)
dsn, _ := pg.ConnectionString(ctx, "sslmode=disable")
m, _ := migrate.New("file://migrations", dsn)
m.Up()
db, _ := gorm.Open(gpostgres.Open(dsn), &gorm.Config{})
return db, func() { ... }
}
Les bénéfices sont concrets.
- Les migrations sont testées avec
golang-migrate, dans le même ordre qu’en production. - Les erreurs de base sont classées sémantiquement, par exemple avec
IsUniqueViolation(err), pas par comparaison de chaînes. - GORM est testé pour de vrai. Je ne découvre pas au moment du déploiement qu’un
sqlmockme racontait une histoire trop propre.
Le tag //go:build integration permet de séparer les tests rapides des tests d’intégration. Je garde go test ./... pour les unit tests rapides, puis go test -tags=integration ./... pour la base réelle.
Choix structurant 2, MSW strict côté React
Côté front, je fais l’inverse d’un mock de fetch au niveau applicatif. Je mocke au niveau réseau avec MSW.
MSW intercepte les requêtes HTTP au niveau du transport, pas au niveau du code métier. Le code de production ne voit pas le mock. Je peux aussi réutiliser les mêmes handlers en test Node et en développement navigateur via Service Worker. Je limite ainsi les écarts entre ce que je teste et ce que je lance en local.
La ligne importante est celle-ci :
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
Avec cette option, toute requête non déclarée fait échouer le test. Sans elle, MSW laisse parfois passer une requête vers une vraie URL. J’obtiens alors des API fantômes, des tests verts qui ne testent pas vraiment le chemin attendu, et un faux sentiment de sécurité.
Je garde aussi un handler par défaut qui répond avec une erreur claire si je n’ai rien déclaré pour la requête.
export const handlers = [
http.all('*', () =>
HttpResponse.json(
{ error: 'no MSW handler declared for this request' },
{ status: 599 },
),
),
];
Chaque test surcharge ensuite les handlers avec server.use(...). Je préfère cette contrainte explicite à un mock permissif qui masque les oublis.
Choix structurant 3, Pact pour le contract testing
Les tests unitaires du front et du back ne voient pas les cassures de contrat entre les deux. Les E2E les voient, mais ils coûtent plus cher, prennent plus de temps, et dépendent d’une infrastructure complète.
Pact comble ce trou. Le pattern est classique, consumer-driven contract testing.
- Le consumer, ici React, écrit un test qui décrit ce qu’il attend de l’API.
- Ce test génère un fichier pact JSON.
- Le provider, ici Go, vérifie qu’il satisfait ce pact.
Côté consumer, je décris l’interaction attendue.
await provider
.addInteraction()
.given('a tariff for prefix 336 exists at rate 1500')
.uponReceiving('a rate request for a French mobile number')
.withRequest('POST', '/api/rate', builder => builder.jsonBody({
duration_seconds: 120,
destination: '+33612345678',
}))
.willRespondWith(200, builder => builder.jsonBody({
cost_millicents: integer(3000),
tariff_prefix: string('336'),
}))
.executeTest(async (mockServer) => {
const result = await rateCall({...}, mockServer.url);
expect(result.tariff_prefix).toBe('336');
});
Côté provider, je vérifie le contrat contre l’API réelle.
verifier.VerifyProvider(t, provider.VerifyRequest{
ProviderBaseURL: "http://localhost:8181",
PactFiles: []string{pactFile},
StateHandlers: models.StateHandlers{
"a tariff for prefix 336 exists at rate 1500": func(setup bool, _ models.ProviderState) (models.ProviderStateResponse, error) {
if setup { seedTariff(336, 1500) }
return nil, nil
},
},
})
Je garde les matchers les plus permissifs possibles. integer(3000) plutôt que 3000, par exemple. Pact me sert surtout à détecter les cassures de contrat au build, pas à remplacer tout le reste.
Le boilerplate à forker
Le squelette complet est dans examples/billing-react-go/ du repo public.
Le domaine est volontairement simple. Un endpoint POST /api/rate calcule le coût d’un appel téléphonique à partir d’un tarif stocké en base. C’est assez petit pour comprendre vite, assez concret pour voir le workflow en vrai.
Je couvre six couches.
- Unit + PBT Go dans
api/pricing/, avectestifypour les tests table-driven etrapidpour trois propriétés, positivité, monotonie, métamorphique. - Handler HTTP Go dans
api/handlers/, avechttptestetgin.Engine, plus unfakeLookupen mémoire. - Repository DB Go dans
api/repository/, avectestcontainers-go,golang-migrateet le tag//go:build integration. - Provider Pact Go dans
api/pacts/, avecpact-goet desStateHandlersqui seedent la base. - Component React dans
frontend/src/components/, avec Vitest, RTL et MSW strict. - Consumer Pact React dans
frontend/src/api/, avecpact-jsV4 et des matchers permissifs.
Je garde aussi une chaîne CI simple.
frontend-unitbackend-unitbackend-integrationmutationsur les packages modifiéscontracte2eseulement surmain
Je ne veux pas un pipeline spectaculaire. Je veux un pipeline lisible.
Lancement local :
git clone https://github.com/mwolff44/spec-to-tests
cd spec-to-tests/examples/billing-react-go
# Backend rapide, sans Docker
cd api && go mod tidy
go test -race ./pricing/... ./handlers/...
# Backend intégration, avec Docker
go test -race -tags=integration ./repository/...
# Frontend
cd ../frontend && pnpm install && pnpm test
# Contract testing cross-stack
pnpm test:pact
cd ../api && go test -tags=pact ./pacts/...
Si je veux monter la pile complète, docker-compose.yml est là pour ça.
Trois points à retenir
1. Je garde une pile de tests simple, mais je la répartis proprement. Vitest, RTL, MSW et fast-check côté front, testify, httptest, testcontainers-go et rapid côté back, Pact en cross-stack, Playwright pour quelques parcours critiques.
2. Trois choix structurants gardent la pile fiable. testcontainers-go plutôt que sqlmock, MSW strict avec onUnhandledRequest: 'error', Pact pour détecter les cassures de contrat au build.
3. L’IA peut produire le code vite. La fiabilité vient du workflow, pas de la vitesse seule. Le boilerplate runnable existe déjà, je peux le cloner et l’adapter.
La semaine prochaine, j’attaque le dernier article de la série. Je parlerai du plan.md persistant, des hooks pre-commit et du passage du plan aux PR.
Pour aller plus loin
- testcontainers-go - golang.testcontainers.org
- golang-migrate - github.com/golang-migrate/migrate
- MSW (Mock Service Worker) - mswjs.io
- Pact documentation - docs.pact.io
- pact-js - github.com/pact-foundation/pact-js
- pact-go - github.com/pact-foundation/pact-go
- Vitest - vitest.dev
- React Testing Library - testing-library.com
- testify - github.com/stretchr/testify
- Gin - gin-gonic.com
- GORM - gorm.io
- fast-check (PBT TS) - fast-check.dev
- rapid (PBT Go) - github.com/flyingmutant/rapid
- Démo runnable React + Go + Pact - github.com/mwolff44/spec-to-tests (
examples/billing-react-go/)
Cet article est le sixième d’une série de sept. L’article précédent traitait du property-based testing. Le prochain parlera du plan.md, des hooks pre-commit et du passage du plan aux PR.