15 - Types et tests : mocks types, fixtures et expect-type
Ce que tu vas apprendre
- Comment typer les mocks sans
anynias unknown as T - Les fixtures typees pour des donnees de test coherentes
- Comment tester les types eux-memes (pas le runtime, le typage)
- Les patterns qui rendent les tests plus fiables et plus lisibles
Prerequisites
Avoir lu les articles sur les utility types et la serie tests fondamentaux.
Le mock qui ment
Un pattern que je vois partout dans les tests TypeScript :
typescriptconst mockUser = {
id: "1",
name: "Test"
} as User
Le problème : User a 8 propriétés. Le mock n'en a que 2. Le as User est un mensonge au compilateur. Si une fonction accede a user.email, le test plante au runtime avec undefined au lieu d'une erreur de compilation.
Pire :
typescriptconst mockService = {} as unknown as UserService
Le double cast as unknown as T est le signe que quelque chose ne va pas. Tu dis au compilateur "fais-moi confiance" alors que l'objet est vide.
Mocks type-safe avec Partial et les factory functions
Partial pour les mocks simples
Si ta fonction n'utilise que certaines propriétés, type le paramètre en consequence :
typescript// Au lieu de mocker tout User, la fonction prend ce dont elle a besoin
function formatUserName(user: Pick<User, "name" | "email">): string {
return `${user.name} <${user.email}>`
}
// Le test n'a besoin que de name et email
test("formatUserName", () => {
const result = formatUserName({ name: "Nicolas", email: "n@n.dev" })
expect(result).toBe("Nicolas <n@n.dev>")
})
Pas de mock, pas de cast. La fonction déclaré exactement ce dont elle a besoin.
Factory functions pour les mocks complets
Quand tu as besoin d'un objet complet, créé une factory :
typescriptfunction createMockUser(overrides: Partial<User> = {}): User {
return {
id: "user_1",
name: "Test User",
email: "test@test.com",
role: "user",
age: 25,
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-01"),
address: null,
...overrides
}
}
test("user is admin", () => {
const admin = createMockUser({ role: "admin" })
expect(isAdmin(admin)).toBe(true)
})
test("user full name", () => {
const user = createMockUser({ name: "Nicolas Nguyen" })
expect(getFullName(user)).toBe("Nicolas Nguyen")
})
La factory fournit des valeurs par défaut pour toutes les propriétés. Les overrides sont types avec Partial<User>. Le compilateur vérifié que les overrides correspondent au type.
Fixtures typees
Pour les donnees de test reutilisables, créé des fichiers de fixtures :
typescript// tests/fixtures/users.ts
import type { User } from "@/types"
export const ALICE: User = {
id: "user_alice",
name: "Alice Martin",
email: "alice@example.com",
role: "admin",
age: 30,
createdAt: new Date("2023-06-15"),
updatedAt: new Date("2024-01-10"),
address: { street: "1 rue de la Paix", city: "Paris", zipCode: "75001" }
}
export const BOB: User = {
id: "user_bob",
name: "Bob Dupont",
email: "bob@example.com",
role: "user",
age: 22,
createdAt: new Date("2024-03-01"),
updatedAt: new Date("2024-03-01"),
address: null
}
Les fixtures sont des valeurs completes et realistes. Si le type User change (ajout d'un champ), le compilateur te montre toutes les fixtures a mettre à jour.
Sur paltemps.fr, chaque entité a un fichier de fixtures dans tests/fixtures/. Les tests importent les fixtures au lieu de créer des objets a la volee.
Mocker des services et des fonctions
Typer les mocks de fonctions
Avec Bun test ou Vitest :
typescriptimport { mock } from "bun:test"
const mockFetch = mock<(id: string) => Promise<User>>(() =>
Promise.resolve(createMockUser())
)
// Le mock est type : mockFetch attend un string et retourne Promise<User>
Mocker un module
typescriptimport { mock } from "bun:test"
mock.module("@/services/email", () => ({
sendEmail: mock<(to: string, subject: string, body: string) => Promise<void>>(
() => Promise.resolve()
)
}))
Le type du mock correspond au type de la vraie fonction. Si la signature change, le mock ne compile plus.
Tester les types : expect-type
Parfois tu veux tester que tes types sont corrects, pas le runtime. La lib expect-type permet ca :
typescriptimport { expectTypeOf } from "expect-type"
test("createUser returns User", () => {
expectTypeOf(createUser).returns.toEqualTypeOf<User>()
})
test("getProperty returns correct type", () => {
const user = createMockUser()
expectTypeOf(getProperty(user, "name")).toBeString()
expectTypeOf(getProperty(user, "age")).toBeNumber()
})
test("Partial makes all props optional", () => {
expectTypeOf<Partial<User>>().toMatchTypeOf<{ name?: string }>()
})
Ces tests verifient les types au moment de la compilation. Si le type change, le test echoue. C'est utile pour les utility types custom et les fonctions génériques.
Verifier les erreurs de type
typescripttest("cannot pass OrderId as UserId", () => {
type UserId = string & { __brand: "UserId" }
type OrderId = string & { __brand: "OrderId" }
// @ts-expect-error — OrderId n'est pas assignable a UserId
const id: UserId = "order_123" as OrderId
})
@ts-expect-error echoue si la ligne suivante compile sans erreur. C'est un test negatif : tu verifies que le compilateur refuse.
Patterns a éviter
Le any dans les tests
typescript// ❌ any propage des bugs silencieux
const mockUser: any = { name: "Test" }
expect(processUser(mockUser)).toBe("Test") // compile, mais mask un bug si processUser utilise user.id
Le as unknown as T
typescript// ❌ Double cast — objet vide pretend etre un service complet
const service = {} as unknown as UserService
Prefere un mock partiel ou une factory.
Les snapshots sur des types
Les snapshots testent des valeurs, pas des types. Si un type change mais que la valeur sérialisée est identique, le snapshot passe. Si la valeur change mais que le type est correct, le snapshot echoue. Utilise expect-type pour les types et les assertions classiques pour les valeurs.
Résumé
- Utilise des factory functions avec
Partial<T>pour des mocks type-safe — pas deas unknown as T - Les fixtures typees detectent les changements de type a la compilation
expect-typeteste les types eux-memes, pas les valeurs — utile pour les utility types et les generics@ts-expect-errorvérifié que le compilateur refuse un code invalide- Si une fonction n'a besoin que de 2 propriétés, type le paramètre avec
Pick— pas besoin de mocker tout l'objet
Article précédent : 14 - Performance du compilateur
Article suivant : 16 - Glossaire
Sources
- expect-type - GitHub par Misha Kaletsky
- Bun Test Documentation
- Vitest - Mocking