TypeScript en pratique - 15 - Types et tests : mocks types, fixtures et expect-type

Comment typer ses tests proprement. Mocks type-safe, fixtures typees, tester les types eux-memes avec expect-type, et les pièges a éviter.

15 - Types et tests : mocks types, fixtures et expect-type

Ce que tu vas apprendre

  • Comment typer les mocks sans any ni as 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 de as unknown as T
  • Les fixtures typees detectent les changements de type a la compilation
  • expect-type teste les types eux-memes, pas les valeurs — utile pour les utility types et les generics
  • @ts-expect-error vé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

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.