Tester et débugger
Mis à jour le 29 juillet 2026
Tester votre app avant la publication
Une application bien testée est une application qui passe la review du Store et qui satisfait ses utilisateurs. Le Apps SDK fournit pour cela une chaîne complète : un environnement local qui imite ChatGPT, des helpers de test pour chaque brique, et un outillage de débogage qui vous dit ce que le modèle a décidé et pourquoi.
Le playground de développement
Le playground est votre premier outil. Il simule l’environnement ChatGPT sur votre machine, sans publication ni compte de test.
# Lancer le playground
npx chatgpt-app dev
# Options disponibles
npx chatgpt-app dev --port 3200 # Port personnalisé
npx chatgpt-app dev --verbose # Logs détaillés
npx chatgpt-app dev --mock-auth # Simuler l'authentification
Vous y retrouvez une interface de conversation simulée, un panneau de logs en temps réel, un inspecteur de widgets et un moniteur de requêtes réseau. L’option --mock-auth mérite une mention particulière : elle vous évite de dérouler tout le flux OAuth à chaque itération, ce qui change radicalement le rythme de travail sur une app authentifiée.
Tester les actions isolément
Le test runner du SDK exécute une action seule, avec des paramètres et un contexte que vous fabriquez. Vous couvrez ainsi le cas nominal, les erreurs métier et le comportement du rate limiting sans dépendre de l’interface.
// tests/actions/weather.test.ts
import { testAction } from "@openai/apps-sdk/testing";
import { getWeather } from "../src/actions/weather";
describe("getWeather", () => {
it("retourne la météo pour une ville valide", async () => {
const result = await testAction(getWeather, {
params: { city: "Paris" },
mockContext: { user: { id: "test-user" } },
});
expect(result.city).toBe("Paris");
expect(result.temperature).toBeDefined();
expect(typeof result.temperature).toBe("number");
});
it("gère les villes inconnues", async () => {
await expect(
testAction(getWeather, {
params: { city: "VilleInexistante123" },
})
).rejects.toThrow("NOT_FOUND");
});
it("respecte le rate limiting", async () => {
// Appeler 21 fois (limite = 20/min)
for (let i = 0; i < 20; i++) {
await testAction(getWeather, { params: { city: "Lyon" } });
}
await expect(
testAction(getWeather, { params: { city: "Lyon" } })
).rejects.toThrow("RATE_LIMITED");
});
});
Vérifier le rendu des widgets
testWidget renvoie l’arbre de composants produit, que vous inspectez comme une structure de données ordinaire. Le premier test contrôle la mise en forme attendue ; le second, plus important, vérifie que le widget survit à des données incomplètes — un champ nul arrivé d’une API défaillante ne doit jamais faire tomber l’affichage dans la conversation.
// tests/widgets/weather-card.test.ts
import { testWidget } from "@openai/apps-sdk/testing";
import { weatherCard } from "../src/widgets/weather-card";
describe("weatherCard", () => {
it("affiche correctement les données météo", () => {
const rendered = testWidget(weatherCard, {
city: "Paris",
temperature: 22,
windSpeed: 15,
condition: "Ciel dégagé",
timestamp: "2026-04-01T10:00:00Z",
});
expect(rendered.type).toBe("card");
expect(rendered.title).toContain("Paris");
// Vérifier les métriques
const metrics = rendered.content.find((c) => c.type === "grid");
expect(metrics.items).toHaveLength(2);
expect(metrics.items[0].value).toBe("22°C");
});
it("gère les données manquantes", () => {
const rendered = testWidget(weatherCard, {
city: "Lyon",
temperature: null,
});
// Le widget ne doit pas crasher
expect(rendered.type).toBe("card");
});
});
Le parcours complet
Actions et widgets peuvent être irréprochables séparément et ne jamais se rencontrer, parce que le modèle n’a pas déclenché l’action attendue. Le test de flow part d’un message utilisateur en langage naturel et vérifie toute la chaîne : action appelée, widget rendu, données transmises.
// tests/flows/weather-flow.test.ts
import { testFlow } from "@openai/apps-sdk/testing";
import { app } from "../src/index";
describe("Weather Flow", () => {
it("affiche la météo après recherche", async () => {
const result = await testFlow(app, {
userMessage: "Quelle est la météo à Marseille ?",
expectedAction: "getWeather",
expectedWidget: "weatherCard",
});
expect(result.actionCalled).toBe(true);
expect(result.widgetRendered).toBe(true);
expect(result.widgetData.city).toBe("Marseille");
});
});
Déboguer en temps réel
Le logger intégré produit des logs structurés, c’est-à-dire des entrées accompagnées de données exploitables plutôt que des chaînes concaténées. Vous tracez l’entrée du handler, le détail des données reçues, et vous journalisez l’erreur technique avant de renvoyer à l’utilisateur un message compréhensible.
import { logger } from "@openai/apps-sdk";
handler: async ({ city }, context) => {
logger.info("Recherche météo", { city, userId: context.user.id });
try {
const data = await fetchWeather(city);
logger.debug("Données reçues", { temperature: data.temp });
return data;
} catch (error) {
logger.error("Erreur API météo", { city, error: error.message });
throw new ActionError("INTERNAL", "Service météo indisponible");
}
}
L’inspecteur réseau du playground complète le tableau en affichant chaque requête HTTP sortante avec son URL, ses en-têtes et son corps, son temps de réponse, son code de statut et le corps de la réponse. Quand cela ne suffit pas, la variable d’environnement CHATGPT_APP_DEBUG ouvre trois niveaux de détail, dont un qui expose les décisions du modèle — c’est celui-là qu’on active lorsqu’une action refuse obstinément de se déclencher.
# Activer les logs détaillés
CHATGPT_APP_DEBUG=true npx chatgpt-app dev
# Logs de chaque décision du modèle
CHATGPT_APP_DEBUG=model npx chatgpt-app dev
# Logs des échanges réseau
CHATGPT_APP_DEBUG=network npx chatgpt-app dev
Quatre erreurs que vous rencontrerez
Ces codes reviennent chez presque tous les développeurs, et chacun a une cause dominante bien identifiée.
| Erreur | Cause probable | Solution |
|---|---|---|
ACTION_NOT_FOUND | Action non enregistrée | Vérifier app.action() dans index.ts |
WIDGET_RENDER_ERROR | Données invalides pour le widget | Valider les données avant le render |
AUTH_TOKEN_EXPIRED | Token OAuth expiré | Implémenter le refresh token |
TIMEOUT | Action trop lente (> 30s) | Optimiser ou découper l’action |
Le TIMEOUT est le plus insidieux : il ne se manifeste presque jamais en local, où vos données de test sont légères, mais surgit en production dès qu’un catalogue réel ou une API tierce lente entre en jeu. Testez donc au moins une fois vos actions contre des volumes réalistes.
Le point de contrôle avant de soumettre
Six vérifications recouvrent l’essentiel de ce qu’un reviewer refera de son côté, et elles se rejouent réellement plutôt qu’on ne s’en souvient. Commencez par le cas nominal de chaque action : toutes doivent retourner des données valides, y compris celles que vous n’appelez plus depuis des semaines. Contrôlez ensuite que les cas d’échec remontent des ActionError explicites et non une exception brute, puis relancez vos widgets sur des données partielles ou nulles — c’est le scénario qui casse le plus souvent une fois l’app publiée.
Restent trois points qui se vérifient en quelques minutes et pèsent lourd dans la décision. L’authentification doit fonctionner de bout en bout, de la connexion initiale jusqu’au refresh du token expiré. Le rate limiting doit être configuré sur les actions sensibles, celles qui appellent une API facturée ou écrivent des données. Et vos tests doivent passer à 100 % : un test rouge que vous avez appris à ignorer est précisément celui qui masquera le prochain vrai bug.
Points clés à retenir
- Le playground simule ChatGPT en local pour tester sans publier
- Testez actions, widgets et flows séparément avec les helpers du SDK
- Utilisez le logger structuré pour un débogage efficace
- Les erreurs courantes ont des solutions connues — consultez la table de référence
- Passez la checklist de pré-soumission avant de publier sur le Store