Tests et tracing avec Claude : unit, functional, integration et end-to-end

Quatrième article de la série « Production Engineering, Evals & Security avec Claude ».

Une eval peut vous dire qu’un système a échoué.

Elle ne vous dit pas nécessairement .

C’est précisément le rôle combiné des tests et du tracing : les tests servent à isoler différents types de défaillances, tandis que les traces permettent de reconstruire l’exécution pour identifier l’étape responsable. Le document distingue quatre niveaux complémentaires : unit, functional, integration et end-to-end.


Pourquoi plusieurs niveaux de tests sont nécessaires

Une application Claude réelle ressemble souvent à ceci :

Input
  ↓
Retrieval
  ↓
Prompt builder
  ↓
Claude API
  ↓
Tool / Parser
  ↓
Output

Le problème est que chaque composant peut fonctionner parfaitement lorsqu’il est testé seul, alors que le workflow complet échoue.

C’est pourquoi il faut tester à plusieurs niveaux.


1. Unit test : vérifier une fonction isolée

Un unit test teste une seule unité de code.

Exemples :

parser
tool wrapper
normalizer
format converter
retry helper

Supposons un parser :

def parse_amount(data):
    return float(data["amount"])

Un test unitaire pourrait vérifier :

def test_parse_amount():
    result = parse_amount({"amount": "42.5"})
    assert result == 42.5

Ce test répond uniquement à la question :

Cette fonction fonctionne-t-elle correctement lorsqu’on lui fournit l’entrée attendue ?

Il ne vérifie pas que le composant précédent produit réellement ce format.


Limite du unit test

Si le composant précédent renvoie :

{"value": "42.5"}

au lieu de :

{"amount": "42.5"}

le parser peut parfaitement réussir tous ses tests unitaires et casser malgré tout en production.

Le unit test vérifie l’unité.

Pas le contrat entre les unités.


2. Functional test : tester un appel Claude

Le document définit le functional test comme un test portant sur un appel Claude et la forme de sa réponse.

Par exemple :

def test_extract_shape():
    result = call_claude(
        "Extract the order date and issue."
    )

    assert "primary_date" in result
    assert "issue" in result

On vérifie que :

Claude call
   ↓
expected response shape

est respecté.

Cela peut couvrir :

  • présence de champs ;
  • type attendu ;
  • réponse parseable ;
  • format structuré.

Ce qu’un functional test ne vérifie pas

Il ne couvre pas nécessairement :

  • le retrieval ;
  • la construction du prompt ;
  • le passage d’un outil à un autre ;
  • la sérialisation d’un résultat ;
  • le workflow complet.

Autrement dit :

Claude fonctionne
≠
le système fonctionne

3. Integration test : tester la jonction entre composants

C’est probablement le niveau le plus important à retenir pour l’examen.

Le document souligne que de nombreuses défaillances silencieuses se trouvent au niveau des handoffs entre composants.

Exemple :

Retrieval
   ↓
Prompt builder

Le retrieval renvoie :

[
    {"content": "Refunds are allowed within 30 days."},
    {"content": "Escalations require manager approval."}
]

Mais le prompt builder attend :

"Refunds are allowed within 30 days..."

Le résultat peut alors être inséré sous une mauvaise forme dans le prompt.


Exemple du document : tous les composants passent, le workflow casse

Le document donne un scénario très représentatif.

Les tests isolés passent :

PASS test_parser_unit
PASS test_extract_shape_functional

Mais l’end-to-end échoue.

La trace montre :

step 1 retrieve(q)
OK
→ 3 chunks as list of dicts

step 2 build_prompt(ctx)
OK
→ raw list inserted

step 3 model.call(prompt)
OK
→ answer ignores context

step 4 assertion
FAIL

La cause est précise :

retrieve()
returns:
list[dict]

build_prompt()
expects:
string

Aucun composant n’est nécessairement cassé en isolation.

Le contrat entre eux est faux.

C’est exactement ce qu’un integration test doit détecter.


Un integration test pour ce cas

On peut tester directement la frontière :

def test_retrieval_to_prompt():
    chunks = retrieve("refund policy")

    prompt = build_prompt(chunks)

    assert "30 days" in prompt

Ou mieux, tester avec de vraies données de retrieval :

def test_retrieval_model_handoff():
    chunks = retrieve("refund policy")

    answer = answer_with_context(chunks)

    assert "30 days" in answer

L’objectif est de vérifier :

output composant A
        ↓
input composant B

avec les formats réels.


4. End-to-end test : tester comme un utilisateur

Un end-to-end test exécute le workflow complet.

Par exemple :

User question
     ↓
Retrieval
     ↓
Prompt
     ↓
Claude
     ↓
Tool calls
     ↓
Parser
     ↓
Final answer

Le test peut vérifier :

def test_full_flow():
    answer = application.ask(
        "How long do I have to request a refund?"
    )

    assert "30 days" in answer

Ce test est particulièrement utile pour détecter :

  • des erreurs d’orchestration ;
  • des problèmes de configuration ;
  • des dépendances mal reliées ;
  • des comportements qui n’apparaissent qu’une fois le système assemblé.

Limite de l’end-to-end

Un E2E vous dit :

FAIL

mais ne dit pas nécessairement :

why

Il peut être difficile de savoir si le problème vient de :

  • retrieval ;
  • prompt ;
  • modèle ;
  • tool ;
  • parser ;
  • réseau ;
  • format.

C’est précisément pourquoi le tracing devient nécessaire.


Tableau des quatre niveaux

NiveauCe qu’il isoleCe qu’il ne peut pas garantir
UnitUne fonction isoléeLes interactions entre composants
FunctionalUn appel ClaudeLe workflow autour de cet appel
IntegrationLe handoff entre composantsTous les comportements globaux
End-to-endLe système completLa localisation précise de la panne

C’est l’une des tables de référence centrales du document.


Le tracing : transformer « ça ne marche pas » en diagnostic

Supposons qu’une eval retourne :

score = 0

Cette information est utile, mais insuffisante.

Avec un trace :

[trace run_id=8f21c]

step 1 retrieve(query)
ok
42 ms
→ 3 chunks

step 2 build_prompt(chunks)
ok
1 ms
→ 1240 tokens

step 3 model.call(prompt)
ok
980 ms
→ answer

step 4 parse(answer)
FAIL
2 ms
→ KeyError: amount

final score: 0

Le diagnostic devient immédiat :

failure
→ step 4
→ parser
→ missing field

Le document résume cette valeur du tracing ainsi : les tests indiquent qu’un échec existe ; la trace montre quelle étape l’a produit.


Que doit enregistrer un trace ?

Selon le document, un trace peut enregistrer :

  • le prompt ;
  • les tool calls ;
  • les résultats intermédiaires ;
  • le timing ;
  • les erreurs ;
  • les étapes successives.

Conceptuellement :

TRACE
│
├── input
├── retrieval output
├── prompt
├── model call
├── tool_use
├── tool_result
├── parser
├── latency
└── final score

L’objectif n’est pas d’accumuler des logs sans structure.

L’objectif est de pouvoir reconstruire la séquence exacte de l’exécution.


Test + trace = diagnostic exploitable

Prenons un scénario :

eval fails

Sans autre information :

Something is wrong.

Avec un E2E :

The workflow is broken.

Avec une trace :

Step 2 passed malformed retrieval data
to step 3.

Avec un integration test spécifique :

The retrieval → prompt contract
is broken.

La progression est donc :

Eval
→ détecte

E2E
→ reproduit

Trace
→ localise

Integration test
→ protège contre la régression

Une règle pratique : transformer chaque incident en test ciblé

Après avoir trouvé un bug de production, il faut éviter qu’il réapparaisse.

Le workflow devient :

incident
   ↓
trace
   ↓
root cause
   ↓
targeted test
   ↓
fix
   ↓
regression protection

Si le bug provient du handoff :

retrieval → prompt

on ajoute un integration test.

S’il provient du parser :

unit test

S’il apparaît uniquement dans le système complet :

end-to-end test

Le niveau du test doit correspondre au niveau du défaut.


Exemple : format contract entre composants

Une bonne pratique implicite derrière l’exemple du document consiste à rendre les contrats explicites.

Mauvaise architecture :

def retrieve(query):
    return [
        {"content": "..."}
    ]

def build_prompt(context):
    return f"Context: {context}"

Ici, personne n’indique clairement le format attendu.

Une version plus robuste peut normaliser :

def format_chunks(chunks):
    return "\n\n".join(
        chunk["content"]
        for chunk in chunks
    )

Puis :

def build_prompt(chunks):
    context = format_chunks(chunks)

    return f"""
    Use the following context:

    {context}
    """

Et surtout :

def test_chunk_contract():
    chunks = retrieve("refund")

    context = format_chunks(chunks)

    assert isinstance(context, str)
    assert "refund" in context.lower()

Le test formalise le contrat.


Le document relie aussi tests et stratégie de retrieval

La section associe également le choix de retrieval à la forme de la tâche.

Deux approches sont distinguées :

fetch once

et :

agentic search

Fetch-once retrieval

Pour une question simple sur un corpus stable :

Question
  ↓
Retrieve once
  ↓
Claude
  ↓
Answer

Exemple :

Quelle est la durée de remboursement indiquée dans cette politique ?

Si la réponse se trouve dans un seul passage connu, plusieurs tours de recherche peuvent être inutiles.


Agentic search

Pour une question plus complexe :

Question
  ↓
Search
  ↓
Read
  ↓
Refine query
  ↓
Search again
  ↓
Synthesize

Cela convient davantage à des questions :

  • multi-step ;
  • nécessitant plusieurs sources ;
  • utilisant un corpus changeant ;
  • nécessitant des recherches successives.

Mais cela augmente :

token cost
+
latency

Le document propose donc éventuellement un routeur.


Router : choisir la stratégie de retrieval

Exemple conceptuel fourni dans le module :

def route(query):
    kind = classify(query)

    if kind == "lookup":
        return fetch_once(query)

    return agentic_search(query)

La logique est :

simple lookup
→ fetch once

multi-step
→ iterative / agentic search

Un petit appel de classification peut être rentable lorsque les requêtes sont réellement mixtes.

En revanche, si toutes les requêtes ont la même forme, le document recommande de ne pas ajouter inutilement ce routeur.


Pourquoi ce sujet appartient à la production engineering

Parce qu’un retrieval trop complexe peut produire :

more calls
more latency
more tokens
more failure points

alors qu’un retrieval trop simple peut produire :

incomplete answer

Le choix devient donc un compromis mesurable entre :

quality
cost
latency
reliability

Les evals déterminent si la stratégie répond suffisamment bien.

Les traces permettent de voir où elle échoue.


Exemple complet de debugging

Supposons :

Question:
"What is the refund deadline?"

Le système répond :

"I don't have enough information."

Étape 1 : eval

FAIL

Étape 2 : trace

retrieve
→ found correct document

build_prompt
→ malformed context

Claude
→ ignored context

Étape 3 : diagnostic

integration failure

Étape 4 : correction

Transformer :

[{"content": "..."}]

en :

"..."

avant injection dans le prompt.

Étape 5 : test ajouté

def test_retrieval_prompt_contract():
    chunks = retrieve(...)

    prompt = build_prompt(chunks)

    assert "30 days" in prompt

On ne se contente donc pas de corriger le bug.

On empêche sa réapparition.


Attention aux tests trop mockés

Le document ne développe pas une doctrine générale sur le mocking, mais son exemple d’intégration illustre un principe important : un test qui utilise uniquement des inputs artificiellement bien formés peut manquer précisément les erreurs présentes dans les données réelles.

Dans le scénario du module :

functional model test

utilise une entrée correcte.

Il passe.

Mais :

real retrieval output

a une autre structure.

C’est uniquement l’integration test utilisant ce format réel qui révèle le problème.

Pour tester un handoff, utilisez autant que possible le contrat réel du composant précédent.


Les tests ne remplacent pas les evals

Il faut distinguer deux questions.

Les tests classiques demandent souvent :

Le système respecte-t-il un contrat technique ?

Par exemple :

JSON parseable ?
field present ?
exception ?

L’eval demande plutôt :

Le comportement est-il suffisamment bon ?

Par exemple :

summary faithful ?
answer complete ?
correct date selected ?

Architecture :

TESTS
→ technical correctness

EVALS
→ behavioral quality

TRACING
→ localization

Les trois sont complémentaires.


Exemple de pipeline de qualité

                 CHANGE
                    │
                    ↓
                Unit tests
                    │
                    ↓
             Functional tests
                    │
                    ↓
             Integration tests
                    │
                    ↓
               E2E tests
                    │
                    ↓
                  Evals
                    │
                    ↓
            Regression detected?
             ┌──────┴───────┐
             │              │
            Yes             No
             │              │
             ↓              ↓
           Trace          Release
             │
             ↓
        Root cause

Ce n’est pas nécessairement une prescription exacte de CI/CD du document, mais cela reflète directement les rôles qu’il attribue aux tests, evals et traces.


Ce qu’il faut retenir pour la certification

Unit test

one function

Exemple :

parser
tool wrapper

Functional test

one Claude call

Vérifie typiquement :

shape
type
parseability

Integration test

component A
     ↓
handoff
     ↓
component B

C’est le test clé pour les erreurs de contrat entre composants.

Le document souligne que beaucoup de défaillances silencieuses apparaissent précisément ici.


End-to-end

user input
→ entire workflow
→ final output

Très bon pour détecter une panne globale.

Moins bon pour la localiser.


Tracing

step-by-step execution history

Il montre :

où
quand
avec quelles données

le workflow a échoué.


Pièges d’examen

Scénario : le parser passe tous ses tests et Claude retourne correctement la structure attendue dans ses functional tests, mais l’application complète échoue parce que la sortie du retrieval n’a pas le format attendu par le prompt builder.

Integration test.


Scénario : vous voulez vérifier uniquement que votre wrapper de tool convertit correctement une exception.

Unit test.


Scénario : vous voulez vérifier qu’un appel Claude retourne un JSON parseable possédant trois champs.

Functional test.


Scénario : vous voulez reproduire exactement ce qu’un utilisateur fait du début à la fin.

End-to-end test.


Scénario : l’end-to-end échoue mais vous ignorez à quelle étape.

→ Examiner le trace.


Scénario : tous les composants fonctionnent individuellement mais le workflow échoue lorsqu’ils sont combinés.

→ Chercher d’abord le handoff / integration seam.


À retenir en une phrase

Les tests répondent à des questions différentes selon leur niveau : unit pour une fonction, functional pour un appel Claude, integration pour les handoffs, end-to-end pour le workflow complet ; le tracing permet ensuite de localiser précisément l’étape responsable d’un échec.

Le phare info – Média indépendant & critique
Sélectionne, organise, contextualise et partage des contenus pertinents autour d’un thème ou d’une problématique, dans une logique de veille, de transmission et de mise en sens.
Pour cet article, l’intelligence artificielle a été utilisée comme un outil d’aide à l’exploration, à la structuration et à la rédaction. Elle permet de confronter plusieurs angles, de repérer certains biais humains possibles et de faire émerger des points de vigilance. Le curateur humain observe aussi les biais possibles de l’IA, vérifie les éléments essentiels, nuance l’analyse, corrige les formulations fragiles et assume la publication.

Articles liés

Fiche de révision certification — Accelerators & IP Contribution

Cette fiche résume les notions essentielles du module Accelerators & IP Contribution. Le fil conducteur est simple : Un build Claude n’est pas terminé lorsqu’il fonctionne....

Étude de cas : corriger un accelerator Claude mal packagé, mal versionné et mal sécurisé

Un système Claude peut fonctionner parfaitement en démonstration et pourtant être impropre à la production. Le cas cumulatif du module illustre précisément cette situation. L’application : s’exécute...

Applications Claude multi-composants : trust boundaries, least privilege et sécurité

Une application Claude moderne n’est souvent pas constituée d’un seul appel API. Elle peut combiner : une API applicative ; Claude ; un agent ; Claude Code ; un MCP...

Comparer les plateformes Claude : latency, compliance, data residency et total cost

Lorsque plusieurs plateformes permettent d’exécuter Claude, comment choisir ? Une comparaison superficielle pourrait se limiter à : la plateforme la moins chère ; celle que l’équipe connaît...

Model versioning avec Claude : pin what ships, eval gates et rollback

Changer de modèle dans une application Claude peut sembler être une modification minime : model = "new-model" Pourtant, en production, ce changement peut modifier : la qualité...

Le sentier du savoir

De la curiosité à la transmission, explorez les étapes qui permettent de transformer l’information en compréhension durable.

Étape 1 — Construire une culture générale solide

Construire une base solide de connaissances pour comprendre le monde. Relier les faits, les disciplines et les repères essentiels.

Étape 2 — Maîtriser la pensée critique et l’analyse : apprendre à penser contre ses propres certitudes

Apprendre à analyser l’information, repérer les biais et questionner les évidences. Penser par soi-même dans un monde saturé de récits.

Étape 3 – Apprendre à argumenter et à convaincre

Structurer sa pensée pour convaincre sans manipuler. Savoir débattre, nuancer et formuler des idées claires.

Étape 4 – Approfondir un ou plusieurs domaines d’expertise

Explorer un ou plusieurs domaines en profondeur. Passer de la curiosité à la compréhension experte.

Etapes 5 : Devenir polyglotte : élargir sa pensée par les langues

Élargir ses horizons par le langage et les cultures. Penser autrement en changeant de langue.

Étape 6 — Comprendre la méthode scientifique et expérimenter

Comprendre la méthode scientifique et l’expérimentation. Distinguer savoirs établis, hypothèses et croyances.

Étape 7 – Écrire, transTransmission : écrire, transmettre, enseigner

Écrire, expliquer, partager ce que l’on a compris. Transformer le savoir en outil collectif.

Étape 8 — Cultiver l’équilibre corps-esprit pour soutenir l’érudition

Cultiver le corps et l’esprit pour soutenir l’érudition dans le temps. Le savoir durable repose aussi sur l’attention et l’équilibre personnel.