🧪 Tests unitaires : Commandes vs Mocks en Go 🔗

Lors de l’écriture de tests unitaires en Go, la gestion des dépendances externes comme les systèmes de fichiers, les horloges système ou les commandes externes peut s’avérer délicate. Cet article explore trois approches pour relever ce défi, en montrant finalement pourquoi le patron de conception Commande (Command Pattern) offre une alternative propre et testable aux simulacres (mocks) complexes.

Examinons un exemple : une fonction conçue pour mesurer les performances (benchmark) de compilations incrémentales.

func Benchmark(binary string, sourcePath string, resultPath string, numBuilds int) error {
	if err := build(binary); err != nil {
		return err
	}
	source, err := read(sourcePath)
	if err != nil {
		return err
	}
	
	deltas := []time.Duration{}
	for i := 0; i < numBuilds; i++ {
		if err := write(sourcePath, edit(source, i)); err != nil {
			return err
		}
		
		start := now()
		if err := build(binary); err != nil {
			return err
		}
		
		delta := now().Sub(start)
		deltas = append(deltas, delta)
		
		if err := revert(sourcePath); err != nil {
			return err
		}
	}
	return write(resultPath, report(deltas))
}

📑 Table des Matières 🔗


⚠️ Le problème 🔗

Cette fonction Benchmark effectue plusieurs opérations :

  1. Elle compile un binaire.
  2. Elle lit un fichier source.
  3. Elle exécute une boucle pour modifier le code source, mesurer le temps de compilation et restaurer le fichier.
  4. Elle écrit le rapport final.

Tester cette fonction directement est difficile car elle repose sur quatre dépendances externes :

De nombreux développeurs pourraient se contenter de tester les fonctions autonomes edit et report, laissant l’orchestration centrale non testée. Voici comment nous pouvons améliorer la conception pour rendre l’ensemble du processus testable.


1️⃣ Approche 1 : Déplacer le code dans Main 🔗

Parfois, vous pouvez faire remonter les dépendances jusqu’à l’appelant (souvent main).

func Benchmark(binary string, sourcePath string, source string, numBuilds int) (string, error) {
	// ... core loop logic remains ...
	// returning the report string instead of writing it
	return report(deltas), nil
}

func Main() error {
	// setup and initial build moved here
	source, _ := read(sourcePath)
	result, _ := Benchmark(binary, sourcePath, source, 3)
	return write(resultPath, result)
}

Verdict : Bien que cela aide, ce n’est pas suffisant ici. La boucle Benchmark a toujours fondamentalement besoin d’interagir avec le système de compilation, l’horloge, le système de fichiers et le contrôle de version à chaque itération.


2️⃣ Approche 2 : Injection de dépendances (Mocks / Fakes) 🔗

L’approche orientée objet traditionnelle est l’injection de dépendances. Vous définissez une interface représentant toutes les actions externes.

type Delegate interface {
	Build(binary string) error
	Read(path string) (string, error)
	Write(path string, content string) error
	Now() time.Time
	Revert(path string) error
}

type BenchmarkRunner struct {
	Delegate Delegate
}

func (b *BenchmarkRunner) Run(binary string, sourcePath string, resultPath string, numBuilds int) error {
	// ... implementation using b.Delegate.Build(), b.Delegate.Now(), etc ...
}

Verdict : Cela fonctionne bien et vous permet de substituer un faux délégué (fake) pour les tests. Cependant, cela nécessite de définir des interfaces et des implémentations simulées pour chaque type d’effet de bord, ce qui peut entraîner une prolifération d’interfaces.


3️⃣ Approche 3 : Le patron de conception Commande (Command Pattern) 🔗

Au lieu de définir des interfaces spécifiques pour chaque action, nous pouvons traiter chaque effet de bord comme une commande générique devant être exécutée par un « exécuteur » (Runner) central.

Tout d’abord, définissons ce que signifie exécuter une commande :

type Runner interface {
	Run(cmd Command) (Result, error)
}

Ensuite, définissons nos commandes spécifiques sous forme de structures simples (ou de constantes) :

type BuildCmd struct { Binary string }
type ReadCmd struct { Path string }
type WriteCmd struct { Path string; Content string }
type NowCmd struct {}
type RevertCmd struct { Path string }

Maintenant, refactorisons la logique de benchmark pour qu’elle renvoie les commandes qu’elle souhaite exécuter, ou qu’elle délègue le contrôle à un exécuteur :

type BenchmarkProcess struct {
	runner Runner
}

func (p *BenchmarkProcess) Execute(binary, sourcePath, resultPath string, numBuilds int) error {
	_, err := p.runner.Run(BuildCmd{Binary: binary})
	if err != nil { return err }
	
	readRes, err := p.runner.Run(ReadCmd{Path: sourcePath})
	if err != nil { return err }
	source := readRes.(string) // Type assertion needed here

	// ... rest of the loop using p.runner.Run() ...
}

💡 Pourquoi les commandes sont meilleures pour les tests 🔗

La véritable puissance de ce patron se révèle dans les tests unitaires. Vous n’avez besoin d’écrire qu’un seul FakeRunner au lieu d’implémenter des interfaces de mock complexes pour chaque dépendance.

Le test devient une simple liste de commandes attendues et de réponses simulées :

func TestBenchmark(t *testing.T) {
	// 1. Define the script: "When you see this command, return this result"
	fakeRunner := NewFakeRunner([]Step{
		{Cmd: BuildCmd{"binary"}, Err: nil},
		{Cmd: ReadCmd{"source_path"}, Res: "source code", Err: nil},
		{Cmd: WriteCmd{"source_path", "edited code"}, Err: nil},
		{Cmd: NowCmd{}, Res: time.Unix(0, 0), Err: nil},
		// ... and so on ...
	})

	p := BenchmarkProcess{runner: fakeRunner}
	err := p.Execute("binary", "source_path", "result_path", 1)
	
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	
	// 2. Verify the runner consumed all expected steps
	if !fakeRunner.IsDone() {
		t.Fatalf("did not execute all expected commands")
	}
}

🎯 Résumé 🔗

En transformant les appels de fonctions en structures de données (Commands), vous découplez votre logique métier de ses effets de bord.


📚 Téléchargez Mon Ebook Gratuit 🔗

Obtenez l'édition hors ligne de Le Mode de Vie de Kaue—un guide pratique pour optimiser 🍏 Santé, 💰 Richesse, ❤️ Amour et 🧘 Sens.

Le Mode de Vie de Kaue

Le Mode de Vie de Kaue

📬 Abonnez-vous à Ma Newsletter 🔗

Recevez une notification lorsque je publie de nouveaux essais, résumés de livres et guides pratiques pour bien vivre. Sans spam—désabonnez-vous à tout moment.


💬 Commentaires et Discussion 🔗

Vous avez une question, une perspective ou une suggestion ? Partagez vos réflexions ci-dessous. Les commentaires fonctionnent avec GitHub Discussions (sans publicité et sans spam)—cliquez sur Se connecter avec GitHub ci-dessous pour commenter ou réagir (créer un compte GitHub gratuit prend moins d'une minute).



Mis à jour le 11 oct. 2026.

AVERTISSEMENT : Le contenu de ce site est fourni à titre purement informatif et éducatif et ne constitue pas un avis médical, psychologique, financier, d'investissement, fiscal ou juridique. Les idées, la sélection et les opinions présentées ici n'engagent que moi et ne représentent pas celles de mon employeur. Les résumés de livres, de conférences et d'articles sont des commentaires éducatifs indépendants et restent la propriété intellectuelle de leurs auteurs respectifs. Des outils d'IA peuvent être utilisés pour aider à l'édition, à la mise en forme et à la synthèse ; bien que tous les efforts soient déployés pour garantir l'exactitude, des erreurs peuvent survenir. CONFIDENTIALITÉ : Ce site utilise des services tiers pour l'abonnement facultatif à la newsletter (follow.it), les commentaires (giscus / GitHub Discussions) et les médias intégrés (YouTube), qui peuvent traiter les adresses IP et les cookies des visiteurs conformément à leurs politiques de confidentialité respectives.