# Testify : assertions lisibles et test suites structurées

> Sixième article de la série Testing Go. La différence cruciale entre assert et require (et pourquoi te tromper coûte cher en debugging), suite.Suite pour les tests stateful, le combo Testify + table-driven tests, et l'opinion qui fâche : quand est-ce que testing.T natif est meilleur que Testify, et pourquoi une partie des projets Go gagneraient à supprimer la dépendance.


*Ou : Comment une API qui se lit comme une phrase finit par écrire des tests qui ne testent rien*

Tu écris un test en Go. Trois lignes : tu setup un input, tu appelles la fonction, tu compares la sortie. Le test natif te force à écrire `if got != want { t.Errorf("got %v, want %v", got, want) }` à chaque fois, et tu te dis qu'il y a forcément mieux. Tu installes Testify. D'un coup, ton test devient `assert.Equal(t, want, got)`. Tu respires. C'est joli. C'est lisible. Tu en mets partout.

Trois mois plus tard, un collègue ouvre ton fichier de tests. Il voit `require.NoError(t, err)` suivi de cinq `assert.Equal`, et il se demande pourquoi la moitié stoppe le test et l'autre non. Il voit `suite.Suite` avec un `SetupTest` et un `SetupSuite` qui font des choses différentes, et il se demande lequel est appelé combien de fois. Il voit `assert.Equal(t, expected, complexStruct)` qui compare deux structs énormes et renvoie un diff illisible. Il ferme le fichier. Il réécrit les tests en natif.

C'est l'histoire de Testify. Un outil qui résout un vrai problème (le `testing` natif est volontairement minimaliste) en en créant un nouveau : tes tests ressemblent à de la prose anglaise, donc tu arrêtes de les lire comme du code, donc tu arrêtes de remarquer quand ils mentent.

Dans [les cinq articles précédents](/blog/ia-code-tu-testes/) de la série — du manifeste sur la valeur ajoutée du testing face à l'IA aux [table-driven tests](/blog/table-driven-tests-go/), en passant par [t.Run et t.Parallel](/blog/subtests-trun-go/), [httptest](/blog/httptest-go-tester-apis-sans-serveur/) et [les interfaces comme mocks](/blog/interfaces-mocking-go/) — on a systématiquement préféré le `testing` natif, parfois avec l'excuse que Testify arriverait « plus tard ». Plus tard, c'est aujourd'hui. On regarde la moitié qui reste : les **assertions** (`assert` et `require`) et les **suites** (`suite.Suite`). C'est la raison pour laquelle la quasi-totalité des projets Go professionnels importent `github.com/stretchr/testify`. C'est aussi la raison pour laquelle une partie non négligeable de ces projets auraient intérêt à le désimporter.

---

## Testify en Go : assertions `assert` vs `require`, suites de tests et pièges à éviter

### La bibliothèque la plus utilisée de l'écosystème Go (et pourquoi)

[Testify](https://github.com/stretchr/testify) est la suite de testing tierce la plus utilisée en Go, par une marge énorme. Scanner les `go.mod` des projets open source un peu sérieux, c'est presque toujours la voir apparaître, souvent en deuxième position après la stdlib. Elle existe depuis une douzaine d'années[^1], elle est maintenue activement, et elle fait quatre choses :

1. **`assert`** — un package d'assertions expressives qui *continuent* en cas d'échec.
2. **`require`** — les mêmes assertions, mais qui *stoppent* le test en cas d'échec.
3. **`mock`** — un système de mocks (qu'on a couvert [dans l'article précédent](/blog/interfaces-mocking-go/)).
4. **`suite`** — des suites de tests avec hooks de setup/teardown.

Aujourd'hui on ne parle que des points 1, 2 et 4. Le mock de Testify, on l'a déjà vu — et de toute façon, pour les mocks, on a conclu qu'il fallait préférer `go.uber.org/mock` ou un fake manuel selon le contexte.

La raison pour laquelle Testify s'est imposé tient en une phrase : le `testing` natif de Go est *délibérément* minimaliste, et ce minimalisme a un coût. `testing.T` te donne `Error`, `Errorf`, `Fatal`, `Fatalf`, `Log`, `Logf`, `Skip`, et c'est à peu près tout. Tu veux comparer deux structs ? Tu écris `if !reflect.DeepEqual(got, want) { ... }` à la main. Tu veux vérifier qu'une slice contient un élément ? Tu écris une boucle. Tu veux vérifier qu'une map a N clés ? Boucle. Tu veux vérifier qu'une erreur est d'un type spécifique ? `errors.As` plus un `if`. Tous ces patterns sont *trivialement* faisables, mais chacun prend trois à cinq lignes au lieu d'une, et le signal est noyé dans le bruit.

Testify dit : *on te donne ces trois-cinq lignes, on les appelle `assert.Equal`, `assert.Contains`, `assert.Len`, `assert.ErrorAs`, et voilà, tes tests font la moitié de la taille et disent trois fois plus clairement ce qu'ils vérifient.*

Le pitch marche. Il marche tellement bien que la question n'est plus « est-ce que j'installe Testify ? » mais « est-ce que j'arrive encore à écrire un test Go *sans* l'utiliser ? ».

---

### `assert` vs `require` : la différence que tout le monde ignore pendant deux ans

Dans Testify, tout existe en double. Chaque fonction qui vit dans le package `assert` existe aussi dans le package `require`, avec la même signature et le même nom. Tu peux écrire :

```go
import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestSomething(t *testing.T) {
    assert.Equal(t, 42, compute())
    require.Equal(t, 42, compute())
}
```

Les deux lignes font visuellement la même chose. Sauf que l'une laisse le test continuer en cas d'échec, et l'autre l'arrête net.

Sous le capot, `assert.Equal` signale l'échec via `t.Errorf` et laisse le test continuer. `require.Equal` fait la même chose, puis appelle `t.FailNow()` pour stopper le test — l'équivalent comportemental d'un `t.Fatalf`, mais en deux étapes. C'est toute la différence. Elle est minuscule en apparence. Elle est énorme en pratique.

Prends ce test, écrit par l'IA pour valider un utilisateur récupéré depuis la base :

```go
func TestGetUser(t *testing.T) {
    user, err := repo.GetUser(42)
    assert.NoError(t, err)
    assert.Equal(t, "Alice", user.Name)
    assert.Equal(t, "alice@example.com", user.Email)
    assert.True(t, user.Verified)
}
```

Si `err` n'est pas nil, `user` est probablement `nil` ou vide. Le premier `assert.NoError` échoue. Le test *continue*. Il tente d'évaluer `user.Name` sur un `*User` nil. Panic. Stack trace illisible. Tu passes quinze minutes à comprendre que le vrai problème, c'était la DB non connectée.

Le test bien écrit :

```go
func TestGetUser(t *testing.T) {
    user, err := repo.GetUser(42)
    require.NoError(t, err)
    require.NotNil(t, user)

    assert.Equal(t, "Alice", user.Name)
    assert.Equal(t, "alice@example.com", user.Email)
    assert.True(t, user.Verified)
}
```

Les pré-conditions (erreur nil, pointeur non nil) utilisent `require` : si l'une des deux échoue, le test s'arrête immédiatement, tu lis un message d'erreur clair au lieu d'une panic. Les vérifications de propriétés indépendantes utilisent `assert` : si `Name` est faux *et* `Email` est faux, tu veux voir les deux messages d'erreur d'un coup, pas juste le premier.

![Deux rails parallèles : sur l'un la bille continue après les drapeaux rouges (assert), sur l'autre elle percute un mur (require)](/images/testify-assert-vs-require.original.webp "assert continue, require stoppe — même signature, deux comportements différents")

La règle pratique :

> **`require`** pour les pré-conditions et les invariants dont la violation rend impossible la suite du test.
> **`assert`** pour les vérifications de propriétés indépendantes dont tu veux connaître *toutes* les violations simultanément.

Cette règle est ignorée par 80 % des projets Go que j'ai lus, et presque systématiquement par l'IA. Le pattern par défaut de l'IA, c'est `assert.X` partout, parce que `assert` est écrit plus souvent dans son corpus d'entraînement. Quand le test échoue avec une panic, elle ne comprend pas pourquoi — et toi non plus, si tu n'as jamais fait attention à la distinction.

Le filtre de review, à appliquer à chaque test que tu touches :

> *Est-ce que la ligne suivante a un sens si celle-ci échoue ?*

Si la réponse est non, la ligne courante doit être un `require`, pas un `assert`.

---

### Les assertions qui font gagner du temps (et celles qui en font perdre)

Testify expose plus de deux cents fonctions d'assertion[^2]. La plupart sont du sucre syntaxique utile. Certaines sont des pièges.

**Celles qui sont objectivement meilleures que le code natif équivalent** :

```go
assert.Equal(t, expected, actual)           // au lieu de reflect.DeepEqual + if
assert.NoError(t, err)                       // au lieu de if err != nil
assert.ErrorIs(t, err, io.EOF)               // au lieu de errors.Is + if
assert.Contains(t, collection, element)      // au lieu d'une boucle
assert.Len(t, slice, 3)                      // au lieu de len() + if
assert.ElementsMatch(t, []int{1,2,3}, got)   // comparaison sans ordre
```

Ces fonctions font *exactement* ce que tu aurais écrit à la main, en une ligne, avec un message d'erreur automatiquement formaté qui te dit lequel des deux arguments est `expected` et lequel est `actual`. C'est un gain net.

**Celles qui sont un piège** :

```go
assert.True(t, user.Age > 18)
```

Techniquement, ça marche. En cas d'échec, le message d'erreur te dit : *"expected true, got false"*. Super. Tu sais maintenant que `user.Age > 18` est faux. Tu ne sais pas si `user.Age` vaut `17`, `-4`, ou `0` parce que la DB n'a rien retourné. Préfère toujours :

```go
assert.Greater(t, user.Age, 18)  // message d'erreur : "17 is not greater than 18"
```

Ou, encore mieux, la forme qui capture la valeur effective :

```go
if user.Age <= 18 {
    t.Errorf("user age: got %d, want > 18", user.Age)
}
```

`assert.True` et `assert.False` sont le signe qu'on n'a pas pris le temps de chercher la bonne assertion Testify — ou qu'on aurait dû rester en natif.

**Celle qui tue vraiment, `assert.Equal` sur de grosses structs** :

```go
assert.Equal(t, expectedOrder, actualOrder)
```

Si les deux `Order` ont dix champs et que trois diffèrent, le message d'erreur te donne un dump textuel des deux structs, ligne par ligne, et tu dois chercher visuellement les différences. Plus la struct est grosse, plus tu perds de temps. Pour les gros objets, `assert.Equal` des champs individuels est presque toujours plus utile :

```go
assert.Equal(t, expectedOrder.ID, actualOrder.ID)
assert.Equal(t, expectedOrder.Total, actualOrder.Total)
assert.Equal(t, expectedOrder.Items, actualOrder.Items)
```

Ou, si tu veux vraiment comparer la struct entière, utilise une lib de diff comme `google/go-cmp` avec un format lisible :

```go
if diff := cmp.Diff(expectedOrder, actualOrder); diff != "" {
    t.Errorf("order mismatch (-want +got):\n%s", diff)
}
```

C'est un peu ironique : la fonction la plus utilisée de Testify (`assert.Equal`) est celle qui vieillit le plus mal quand la complexité du test augmente.

---

### `suite.Suite` : la vieille idée OOP qui revient par la fenêtre

Go n'a pas de classe. Go n'a pas d'héritage. Go essaie activement de te décourager de faire de l'OOP. Et pourtant, `testify/suite` te permet d'écrire ça :

```go
type UserRepoSuite struct {
    suite.Suite
    db   *sql.DB
    repo *UserRepository
}

func (s *UserRepoSuite) SetupSuite() {
    s.db = openTestDB(s.T())
    runMigrations(s.T(), s.db)
}

func (s *UserRepoSuite) TearDownSuite() {
    s.db.Close()
}

func (s *UserRepoSuite) SetupTest() {
    s.repo = NewUserRepository(s.db)
    truncateAllTables(s.T(), s.db)
}

func (s *UserRepoSuite) TestCreateUser_Success() {
    err := s.repo.Create(User{Name: "Alice"})
    s.Require().NoError(err)
    s.Equal(1, s.repo.Count())
}

func (s *UserRepoSuite) TestCreateUser_Duplicate() {
    s.repo.Create(User{Name: "Alice"})
    err := s.repo.Create(User{Name: "Alice"})
    s.Error(err)
}

func TestUserRepoSuite(t *testing.T) {
    suite.Run(t, new(UserRepoSuite))
}
```

C'est très clairement du JUnit. Le pattern `SetupSuite` / `TearDownSuite` tourne une fois par suite, `SetupTest` / `TearDownTest` tourne avant et après chaque test. Les tests sont des méthodes dont le nom commence par `Test`. `s.Equal`, `s.Require().NoError`, etc. sont des raccourcis qui remplacent `assert.Equal(s.T(), ...)`.

![Étagères de laboratoire : flacons lourds quasi immobiles en haut (SetupSuite), tubes échangés rapidement par des bras mécaniques au milieu (SetupTest)](/images/testify-suite-setup-teardown.original.webp "SetupSuite : une fois par suite. SetupTest : avant chaque test. Confondre les deux, c'est multiplier par cent le temps de tes tests.")

**Ce qui est utile** :

- Tu as de la **vraie state partagée** entre les tests qui doit être setup/teardown dans un ordre précis (connexion DB, containers, processus externes).
- Tu as une **configuration de test complexe** (fixtures, données de seed) que tu factorises dans des méthodes d'helper qui partagent l'état de la suite.
- Tu veux **grouper** logiquement des tests qui testent le même sujet sans avoir à répéter le setup dans chaque fonction `TestXXX`.

**Ce qui est un piège** :

- Tu crées des suites pour **regrouper des tests qui n'ont aucun state partagé**. À ce moment-là, tu payes le coût cognitif de l'abstraction (héritage implicite, ordre d'exécution des hooks, `s.T()` omniprésent) pour rien. Une simple fonction `func setupTest(t *testing.T) ...` aurait fait le job en natif.
- Tu confonds `SetupSuite` et `SetupTest`. Ça arrive souvent. Tu ouvres une connexion DB dans `SetupTest` au lieu de `SetupSuite`, et tes tests prennent dix secondes au lieu d'un dixième de seconde. Ou l'inverse : tu seed les données dans `SetupSuite`, le premier test les modifie, et les tests suivants échouent en cascade.
- Les hooks **imbriqués avec `t.Parallel()`** sont un nid à bugs. Les tests parallélisés partagent l'état de la suite, et si tu n'as pas prévu des locks, tu as des races. Testify ne t'aide pas à les détecter[^3].
- La **composition entre suites** est théoriquement possible (tu peux embarquer une suite dans une autre) mais quasi jamais bien faite en pratique. Tu finis par dupliquer le setup.

La règle : les suites de Testify sont utiles quand tu testes un *composant stateful* (repository, service avec cache persistant, wrapper autour d'une ressource externe). Pour tout le reste — et c'est la majorité — tu n'en as pas besoin.

---

### Le combo gagnant : Testify + table-driven tests

[On a vu les table-driven tests](/blog/table-driven-tests-go/) dans le deuxième article. On a vu les assertions Testify aujourd'hui. Les deux se combinent naturellement, et c'est là que Testify est le plus clairement gagnant face au natif.

Version native :

```go
func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  error
    }{
        {"valid simple", "alice@example.com", nil},
        {"missing at", "aliceexample.com", ErrMissingAt},
        {"empty", "", ErrEmpty},
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            err := ValidateEmail(tc.input)
            if !errors.Is(err, tc.want) {
                t.Errorf("err: got %v, want %v", err, tc.want)
            }
        })
    }
}
```

Version Testify :

```go
func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  error
    }{
        {"valid simple", "alice@example.com", nil},
        {"missing at", "aliceexample.com", ErrMissingAt},
        {"empty", "", ErrEmpty},
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            err := ValidateEmail(tc.input)
            assert.ErrorIs(t, err, tc.want)
        })
    }
}
```

L'assertion fait en une ligne ce qui prenait trois lignes en natif, et le message d'erreur est automatiquement formaté avec le nom du sous-test en préfixe. C'est presque toujours un gain net — et c'est probablement le seul contexte où je recommande Testify sans nuance.

Le piège, ici, c'est de mettre `require` au lieu de `assert` *dans la boucle*. Un `require` qui échoue dans un sous-test stoppe le sous-test *et* la boucle. Tu perds la visibilité sur les cas suivants. Utilise `assert` par défaut dans les table-driven tests — sauf si le cas courant a plusieurs vérifications séquentielles dont la première conditionne les suivantes.

> **Junior Jules :** J'ai migré tous nos tests vers Testify la semaine dernière. On a perdu 30 % du volume de code.
>
> **Senior Sam :** C'est une migration ou un refactoring ?
>
> **Junior Jules :** Une migration. J'ai juste remplacé `if got != want { t.Errorf(...) }` par `assert.Equal`.
>
> **Senior Sam :** Tu as remplacé partout ?
>
> **Junior Jules :** Partout.
>
> **Senior Sam :** Y compris dans les tests où on vérifiait cinq propriétés après un `err := repo.Get(...)` qui pouvait retourner nil ?
>
> **Junior Jules :** ...ah.
>
> **Senior Sam :** Donc t'as aussi introduit des panics quand la DB est down. Tu sais combien d'heures ça va nous coûter de remplacer les `assert.NoError` par `require.NoError` au bon endroit ?
>
> **Junior Jules :** Je commence quand ?

---

### L'opinion impopulaire : quand supprimer Testify

Une partie non négligeable des projets Go gagneraient à *désimporter* Testify. Pas tous. Une partie. Les signaux :

**1. Le projet n'utilise `assert.Equal`, `assert.NoError`, et `assert.NotNil` — et rien d'autre.**

Si tu n'utilises que trois à cinq fonctions de Testify, tu payes une dépendance externe pour ce qui pourrait être trois fonctions d'helper locales de dix lignes chacune. Le coût d'import (taille du binaire, surface d'attaque supply-chain[^4], temps de compilation) n'est pas nul. Le natif + un fichier `testhelpers/assertions.go` de 50 lignes te donne les mêmes ergonomiques sans la dette.

**2. Le projet a zéro suite.Suite, zéro require, et zéro mock.**

Si Testify est importé uniquement pour les assertions, et qu'aucun test n'utilise require (voir point 1 sur la confusion assert/require), alors personne dans l'équipe n'a vraiment réfléchi à ce que Testify fait. La dépendance a été ajoutée par défaut.

**3. Les tests sont saturés d'`assert.True` et d'`assert.Equal` sur des grosses structs.**

C'est le signe que l'équipe utilise Testify comme un réflexe au lieu de choisir l'assertion la plus précise. Retirer Testify force à écrire le test que tu aurais dû écrire au départ — et souvent à comprendre *quel contrat* on teste au lieu de taper « compare ces deux choses » sans réfléchir.

**4. Les tests sont générés à 80 % par l'IA.**

Si ton pipeline de test est « demander à Claude/Copilot de générer un test, le commit tel quel », Testify amplifie le problème. L'IA est *à l'aise* avec Testify. Elle en produit des volumes industriels. Retirer Testify ralentit l'IA — et t'oblige, toi, à relire ses tests en code Go idiomatique. Ce qui est précisément le job.

À l'inverse, **garde Testify** si :

- Tu as de vrais tests d'intégration avec du state (DB, containers) et tu utilises `suite.Suite` proprement.
- Ton équipe connaît la distinction `assert`/`require` et l'applique systématiquement.
- Tu utilises `assert.ErrorIs`, `assert.ElementsMatch`, `assert.JSONEq` et d'autres assertions non triviales que tu devrais sinon réimplémenter.
- Tu utilises `mock.Mock` et tu n'es pas prêt à migrer vers `go.uber.org/mock` ou des fakes manuels.

La question n'est pas « Testify est-il bon ? » (il est bon). La question est « Testify est-il bon *pour ce projet, utilisé comme ça* ? ». Pour une partie des projets Go, la réponse honnête est non.

---

### Migrer dans les deux sens

Une migration **natif → Testify** est triviale et largement automatisable avec sed. Une migration **Testify → natif** l'est aussi, à condition d'avoir une table de correspondance claire :

| Testify | Natif équivalent |
|---|---|
| `assert.Equal(t, a, b)` | `if !reflect.DeepEqual(a, b) { t.Errorf(...) }` |
| `assert.NoError(t, err)` | `if err != nil { t.Errorf(...) }` |
| `require.NoError(t, err)` | `if err != nil { t.Fatalf(...) }` |
| `assert.ErrorIs(t, err, target)` | `if !errors.Is(err, target) { t.Errorf(...) }` |
| `assert.Contains(t, s, sub)` | `if !strings.Contains(s, sub) { t.Errorf(...) }` |
| `assert.Len(t, slice, n)` | `if len(slice) != n { t.Errorf(...) }` |
| `suite.Suite` + méthodes `TestXXX` | fonctions `TestXXX` + helper `setupXXX(t)` |

Une heure de grep et sed, une passe de `go test ./...`, et ton projet compile à nouveau sans Testify. Le nombre de lignes augmente de 20-30 %. La lisibilité *peut* baisser si tu ne fais rien — mais souvent, elle s'améliore, parce que le test explicite ce qu'il vérifie au lieu de le cacher derrière une assertion qui *ressemble* à de la prose.

---

### Ce que tu peux faire maintenant

1. Ouvre ton projet. `grep -rn "assert\." --include="*_test.go" | wc -l` et `grep -rn "require\." --include="*_test.go" | wc -l`. Si le ratio require/assert est inférieur à 10 %, ton équipe n'a pas assimilé la distinction.
2. Cherche les `assert.True` et `assert.False`. Chacun est un test qui pourrait être remplacé par une assertion plus précise (`Greater`, `Contains`, `ErrorIs`…) ou qui aurait dû être un `if` natif explicite.
3. Cherche les `assert.Equal` sur des structs de plus de cinq champs. Vérifie que chaque champ testé a un sens au regard du contrat. Si trois des dix champs sont des timestamps auto-générés, l'assertion va casser au premier refactoring.
4. Si tu as des `suite.Suite`, vérifie que le `SetupSuite` ne fait *que* du setup coûteux (DB, containers) et que le `SetupTest` ne fait que du setup léger (reset de state). Si c'est l'inverse, tes tests sont plus lents qu'ils ne devraient l'être — sensiblement.
5. Fais le calcul honnête : sur une semaine, combien de fonctions distinctes de Testify ton projet utilise-t-il ? Si c'est moins de huit, le projet est candidat à une migration vers natif + un fichier d'helpers locaux.

---

### La suite

On a vu les table-driven tests, les subtests, httptest, les mocks, et maintenant Testify. C'est la boîte à outils complète pour tester du code Go *isolé*. Mais le code isolé n'est pas le code qui casse en production. Ce qui casse en production, c'est le code qui interagit avec des inputs qu'on n'a pas prévus.

Dans le prochain article, on sort des sentiers battus. On regarde le **fuzzing natif** de Go — la feature intégrée à la stdlib depuis Go 1.18 qui te permet de générer des millions d'inputs aléatoires pour trouver les bugs que personne, toi y compris, n'a imaginés. C'est l'arme la plus puissante de la panoplie de testing Go, et c'est aussi celle que l'IA ne sait toujours pas utiliser — parce que ses tests sont basés sur des cas qu'elle *anticipe*, alors que le fuzzing trouve précisément ceux qu'aucun humain n'aurait anticipés.

<div class="next-article">
<a href="/blog/fuzzing-natif-go/">
<span class="next-article__label">Article suivant de la série</span>
<p class="next-article__title">Fuzzing natif en Go : trouver les bugs que personne n'a imaginés</p>
<p class="next-article__desc">testing.F, seed corpus, fuzzing de handlers HTTP, et pourquoi c'est l'arme ultime contre le code généré par IA.</p>
</a>
</div>

[^1]: Le repo `github.com/stretchr/testify` a été créé en 2012 par Mat Ryer et Tyler Bunnell sous l'organisation Stretchr, puis repris par une équipe de mainteneurs de la communauté Go. Une `v2` a été longuement discutée (épisode Go Time #139, pour les curieux) mais la décision actuelle du mainteneur est claire : pas de `v2` breaking sur le repo officiel, la v1.x reste la référence. Au moment d'écrire, la dernière version stable est la v1.11.x, sortie fin 2025. Les évolutions récentes concernent surtout l'ajout d'assertions spécialisées (`ErrorContains`, `EventuallyWithT`, `EqualExportedValues`), pas des changements d'API existante.

[^2]: Plus précisément, le package `assert` de la v1.11 expose plus de deux cents fonctions exportées — entre les assertions de base, leurs variantes formattées (`Equalf`, `Truef`), les variantes HTTP/JSON/YAML/filesystem, et les méthodes sur le type `Assertions`. Personne ne les connaît toutes — et personne n'a besoin de les connaître. Le sous-ensemble utile tient sur une fiche A5 : `Equal`, `NotEqual`, `NoError`, `Error`, `ErrorIs`, `ErrorAs`, `ErrorContains`, `NotNil`, `Nil`, `True`, `False`, `Contains`, `NotContains`, `Len`, `Empty`, `NotEmpty`, `ElementsMatch`, `Greater`, `Less`, `InDelta`, `JSONEq`, `EventuallyWithT`. Tout le reste, c'est du plumage.

[^3]: Le [go race detector](https://go.dev/doc/articles/race_detector) fonctionne bien sûr dans les suites Testify — `go test -race` reste obligatoire. Le problème, c'est qu'il détecte les data races *quand elles se produisent*, pas les erreurs de design de la suite. Un `SetupSuite` qui initialise un état mutable partagé par vingt tests parallèles est un bug d'architecture que seule une relecture humaine détecte. `-race` ne t'aidera qu'après coup, quand le test sera flaky en CI.

[^4]: La supply chain côté Testify est raisonnablement saine — le repo est sur l'organisation `stretchr/`, l'historique de contributions est dense et publiquement auditable, et les dépendances restent limitées : `davecgh/go-spew` et `stretchr/objx` en directes, `pmezard/go-difflib` et `gopkg.in/yaml.v3` en indirectes. Mais le principe général tient : chaque dépendance ajoutée à `go.mod` est une surface d'attaque supplémentaire, et les libs de testing sont particulièrement intéressantes pour les attaquants parce qu'elles sont souvent moins auditées que le code de production. Voir [l'incident XZ Utils](https://en.wikipedia.org/wiki/XZ_Utils_backdoor) de 2024 pour un rappel de ce qui peut arriver quand une dépendance « stable depuis dix ans » passe entre de mauvaises mains.


