1. O que realmente testamos no ChatGPT App (e o que não testamos)
Em um aplicativo web clássico está tudo claro: UI → backend → BD. Escrevemos testes unitários para funções, de integração — para APIs, e E2E — para “o usuário percorreu o fluxo”.
No ChatGPT App o cenário é um pouco mais complexo:
Usuário ↔ ChatGPT UI ↔ Widget (Apps SDK, React)
↘
Servidor MCP (tools/resources)
↘
ACP / backend / APIs externas
O modelo dentro do ChatGPT decide quando chamar seu suggest_gifts, com quais argumentos, como renderizar o structuredContent vindo do MCP e quando mostrar seu widget.
Do ponto de vista de testes, é conveniente dividir o mundo em duas camadas:
- Infrastructure tests — é disso que tratamos nesta aula. Verificamos que:
- o código do widget não quebra quando o usuário clica;
- as MCP‑tools aceitam e retornam dados no formato prometido pelos schemas;
- os endpoints ACP e os webhooks estão vivos e não falham com um JSON típico.
- AI behavior evals — isso ficará no Módulo 20. Lá veremos o que exatamente o modelo responde: se explica de forma adequada, se escolhe o presente corretamente em termos de significado, se não alucina etc.
Uma fórmula grosseira para hoje:
“Testamos tudo ao redor da LLM, mas não a própria LLM”.
Por isso, no plano do curso para este tema destacamos: “Não testamos a resposta do GPT literalmente, testamos a infraestrutura ao redor e os contratos de dados”.
Para não nos perdermos, usamos uma “pirâmide” simples de testes para o GiftGenius.
graph TD A["Testes unitários
utils, lógica de negócios das tools"] --> B[Testes de contrato
Zod/JSON Schema, webhooks] B --> C[E2E / Testes de UI
widget + MCP sem ChatGPT] C --> D["Smoke no CI
“isso está vivo mesmo?”"] style A fill:#e0f7fa,stroke:#00838f,stroke-width:1px style B fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px style C fill:#fff3e0,stroke:#ef6c00,stroke-width:1px style D fill:#ffebee,stroke:#c62828,stroke-width:1px
Agora vamos passar por cada nível e, ao mesmo tempo, ampliar nosso GiftGenius de treinamento com testes. No fim, montaremos um checklist de erros típicos que aparecem com mais frequência ao testar um ChatGPT App.
2. Testes unitários: dividindo o GiftGenius em pedaços pequenos
O que considerar como “unit” no ChatGPT App
No nosso stack, um teste unitário é a verificação de uma pequena parte isolada da lógica. Sem rede real, sem banco de dados e, se possível, sem chamar o próprio framework MCP.
No GiftGenius isso pode ser:
- uma função que calcula a “relevância do presente”;
- um filtro que remove produtos sem preço ou com moeda inadequada;
- um conversor de moeda;
- um mapper de um objeto “cru” de produto para GiftCardProps para o UI.
Idealmente, a lógica das próprias MCP‑tools também deve ser dividida: o handler de rota do MCP é um invólucro fino que chama uma função pura com a lógica de negócios. Nos testes unitários, testamos a função pura.
Exemplo: função de ranqueamento de presentes
Suponha que temos a utilitária scoreGift que, com base no intervalo de preços e na popularidade, atribui uma “nota”:
// src/lib/scoreGift.ts
export type Gift = {
id: string;
price: number;
popularity: number; // 0..1
};
export function scoreGift(gift: Gift, maxPrice: number): number {
if (gift.price > maxPrice) return 0;
const priceScore = 1 - gift.price / maxPrice;
return Math.round((priceScore * 0.6 + gift.popularity * 0.4) * 100);
}
Vamos escrever um teste unitário no Jest (no Vitest será quase igual):
// src/lib/scoreGift.test.ts
import { scoreGift } from './scoreGift';
test('scoreGift reduz a nota para presentes caros', () => {
const cheap = { id: 'c', price: 50, popularity: 0.5 };
const expensive = { id: 'e', price: 100, popularity: 0.5 };
const max = 100;
const cheapScore = scoreGift(cheap, max);
const expensiveScore = scoreGift(expensive, max);
expect(cheapScore).toBeGreaterThan(expensiveScore);
});
Aqui vemos o básico “Arrange–Act–Assert” (preparamos os dados, chamamos a função, verificamos o resultado) — exatamente a estrutura recomendada também para testes mais complexos.
Extraindo a lógica de negócios do handler do MCP
Hoje você provavelmente tem algo como:
// app/mcp/route.ts — bem simplificado
import { createMcpServer } from '@modelcontextprotocol/sdk';
import { scoreGift } from '@/lib/scoreGift';
server.tool('suggest_gifts', {
// ...
handler: async ({ input }) => {
const gifts = await fetchFromCatalog(input);
const scored = gifts
.map(g => ({ ...g, score: scoreGift(g, input.maxPrice) }))
.sort((a, b) => b.score - a.score);
return { gifts: scored.slice(0, 10) };
},
});
O teste unitário para scoreGift já escrevemos, mas também queremos testar a função inteira: “a função que pega uma lista de presentes e retorna o top‑10 ordenado”. Vamos extraí‑la para um módulo separado:
// src/lib/rankGifts.ts
import { scoreGift, Gift } from './scoreGift';
export function rankGifts(gifts: Gift[], maxPrice: number) {
return gifts
.map(g => ({ ...g, score: scoreGift(g, maxPrice) }))
.sort((a, b) => b.score - a.score)
.slice(0, 10);
}
E o teste:
// src/lib/rankGifts.test.ts
import { rankGifts } from './rankGifts';
test('rankGifts retorna no máximo 10 presentes em ordem decrescente de score', () => {
const gifts = Array.from({ length: 20 }, (_, i) => ({
id: `g${i}`,
price: 10 + i,
popularity: 0.5,
}));
const result = rankGifts(gifts, 100);
expect(result).toHaveLength(10);
expect(result[0].score).toBeGreaterThanOrEqual(result[9].score);
});
Testes unitários assim são rápidos, baratos e dão feedback imediato — por isso são recomendados como a “base larga da pirâmide de testes” para serviços MCP.
Testes unitários para MCP tools: mockando APIs externas
Um erro comum é tentar “testar unitariamente” o handler da MCP‑tool junto com requisições HTTP reais para o catálogo, Stripe, etc. Como resultado, o teste fica lento e frágil.
A melhor opção: deixar no handler apenas a “cola” (wiring) e extrair toda a lógica complexa para funções que já testamos separadamente. Se você realmente quiser testar o próprio handler, substitua as dependências por mocks. É exatamente o que os guias detalhados de testes de MCP recomendam: mockar APIs externas nos tool‑handlers.
3. Testes de contrato: Zod/JSON Schema como “acordo” com o modelo e com o ACP
O que é um teste de contrato no nosso contexto
A lógica unitária resolvemos: funções puras pequenas estão sob controle. A próxima camada da pirâmide é garantir que os serviços ainda se entendem pelos contratos JSON. Isso são os testes de contrato.
Teste de contrato é a verificação de que duas partes que trocam dados ainda se entendem. O foco não está nos algoritmos internos, mas na forma e no significado do JSON: campos, tipos, obrigatoriedade.
No ChatGPT App temos vários desses contratos:
- ChatGPT ↔ MCP: inputSchema e outputSchema das MCP‑tools.
- MCP ↔ commerce‑API (ACP): formato das requisições create_checkout_session, estrutura das respostas.
- ACP ↔ nosso backend via webhooks: order.created, payment_failed etc.
Se você altera o schema, mas esquece de atualizar o código (ou vice‑versa — muda o código e deixa o schema antigo), surge uma quebra silenciosa. O modelo continua enviando o JSON antigo, mas seu código já espera um campo novo — e cai em runtime. É exatamente esse tipo de situação que os testes de contrato devem capturar antes da produção.
Zod como fonte única da verdade
No ecossistema JavaScript/TypeScript, o Zod encaixa perfeitamente para isso, e você já o usou com MCP: o SDK sabe converter schemas Zod em JSON Schema para declarar ferramentas.
Por exemplo, vamos descrever o schema de um presente e do resultado da recomendação:
// src/schemas/gift.ts
import { z } from 'zod';
export const GiftSchema = z.object({
id: z.string(),
title: z.string(),
price: z.number().nonnegative(),
currency: z.string().length(3),
url: z.string().url(),
});
export const SuggestGiftsResultSchema = z.object({
gifts: z.array(GiftSchema).min(1),
});
Tipos para o código obtemos via z.infer:
export type Gift = z.infer<typeof GiftSchema>;
export type SuggestGiftsResult = z.infer<typeof SuggestGiftsResultSchema>;
Isso já é um tipo de teste de contrato em compile‑time: se em algum lugar do código você tentar atribuir currency: 123, o TypeScript vai avisar que deveria ser string.
Testes de contrato em runtime para os schemas
Testes em runtime nos protegem ainda mais, pois processam exemplos reais (ou próximos do real) de dados pelos schemas.
// src/schemas/gift.test.ts
import { GiftSchema, SuggestGiftsResultSchema } from './gift';
test('GiftSchema aceita um produto válido', () => {
const sample = {
id: '123',
title: 'Caneca com gato',
price: 19.99,
currency: 'USD',
url: 'https://example.com/gift/123',
};
expect(() => GiftSchema.parse(sample)).not.toThrow();
});
test('SuggestGiftsResultSchema rejeita uma lista vazia de presentes', () => {
const badResult = { gifts: [] };
expect(() => SuggestGiftsResultSchema.parse(badResult)).toThrow();
});
Por que isso é importante:
- se você mostra exemplos de JSON para o modelo nos prompts/documentação, pode colocá‑los diretamente nesses testes e garantir que “o exemplo não mente”;
- se você alterar o schema (por exemplo, tornar o campo url obrigatório), os testes vão apontar todos os exemplos e fixtures antigos que deixaram de ser válidos.
As recomendações oficiais do Apps SDK enfatizam: structured content deve corresponder ao outputSchema declarado; caso contrário, o modelo pode não entendê‑lo. Testes de schemas são a primeira linha de defesa para evitar divergências.
Contratos de webhooks e ACP
O mesmo princípio vale para webhooks e endpoints do ACP. Suponha que temos OrderCreated:
// src/schemas/acp.ts
import { z } from 'zod';
export const OrderCreatedSchema = z.object({
id: z.string(),
userId: z.string(),
totalAmount: z.number(),
currency: z.string().length(3),
status: z.literal('created'),
});
Teste:
// src/schemas/acp.test.ts
import { OrderCreatedSchema } from './acp';
test('OrderCreatedSchema valida o sample do webhook', () => {
const sample = {
id: 'ord_1',
userId: 'user_42',
totalAmount: 59.99,
currency: 'USD',
status: 'created',
};
expect(() => OrderCreatedSchema.parse(sample)).not.toThrow();
});
Depois, no handler do webhook, a primeira coisa é fazer OrderCreatedSchema.parse(body) — e você já tem certeza de que trabalhará com um objeto válido.
A OpenAI, no checklist de regressão para Apps, também recomenda manter os schemas atualizados conforme o app evolui — testes de contrato garantem que você não vai esquecer disso.
4. Testando o widget e “quase E2E”: como fazer sem o chatgpt.com
Testes unitários mantêm a lógica em ordem, os testes de contrato — a forma dos dados entre serviços. Mas a pirâmide não termina aí: ainda precisamos verificar que todo o caminho do usuário pelo widget e MCP realmente funciona como um todo. Para um ChatGPT App, isso será um formato especial, “quase E2E”.
Por que não dá para simplesmente “rodar o Playwright” no ChatGPT
O impulso intuitivo: “Vamos abrir https://chatgpt.com, iniciar o widget, percorrer todo o cenário ‘escolher presente → finalizar compra’ com Playwright, e teremos um E2E de verdade”.
Infelizmente, não.
Problemas:
- rodar testes automatizados contra o chatgpt.com viola os ToS;
- há proteção (Cloudflare, 2FA etc.) que não gosta de bots vindos do CI;
- o comportamento do modelo é variável: hoje ele chama seu suggest_gifts, amanhã decide se limitar a uma resposta textual.
Portanto, para um ChatGPT App o E2E é entendido de forma mais ampla: testamos o caminho completo dentro do seu aplicativo — widget + MCP + ACP — mas sem o ChatGPT UI real e sem o modelo real.
Nos guias detalhados, a estratégia recomendada é: testar o servidor MCP separadamente com um cliente headless, e o widget — em um “host de teste” com window.openai mockado.
Testando o widget como um componente React
A opção básica — React Testing Library. Precisamos:
- Abrir o componente GiftGeniusWidget.
- Injetar um window.openai falso com os métodos necessários (callTool, openExternal etc.).
- Simular o usuário: clicar em botões, digitar texto.
- Verificar que callTool foi chamado com os argumentos corretos e que o UI mostra o resultado esperado.
Suponha que temos um widget simplificado:
// src/app/GiftGeniusWidget.tsx
'use client';
import React from 'react';
export function GiftGeniusWidget() {
const [loading, setLoading] = React.useState(false);
async function handleClick() {
setLoading(true);
await (window as any).openai.callTool('suggest_gifts', {
occasion: 'birthday',
});
setLoading(false);
}
return (
<div>
<button onClick={handleClick}>Escolher presente</button>
{loading && <p>Um segundo, buscando ideias...</p>}
</div>
);
}
Teste:
// src/app/GiftGeniusWidget.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { GiftGeniusWidget } from './GiftGeniusWidget';
test('o botão chama suggest_gifts via window.openai.callTool', async () => {
const callToolMock = vi.fn().mockResolvedValue({});
(window as any).openai = { callTool: callToolMock };
render(<GiftGeniusWidget />);
const button = screen.getByText('Escolher presente');
await fireEvent.click(button);
expect(callToolMock).toHaveBeenCalledWith('suggest_gifts', {
occasion: 'birthday',
});
});
Aqui controlamos completamente o ambiente:
- nada de ChatGPT real;
- nada de rede;
- teste limpo e rápido, que verifica a ligação “UI → window.openai”.
A documentação do Apps SDK recomenda isso: mockar window.openai ao testar o widget, para não depender do ambiente real.
E2E‑light com Playwright: Next.js + MCP
O próximo nível — subimos localmente o aplicativo Next.js (como no Dev Mode), mas acessamos diretamente pelo navegador do teste, não via ChatGPT.
Cenário que faz sentido verificar:
- Abrir a página /widget (ou / — conforme seu projeto).
- Simular o mínimo de passos: escolher o tipo de presente, clicar no botão “Mostrar ideias”.
- Verificar que o widget mostrou cards de presentes.
- (Opcional) clicar em um card, clicar em “Ir para pagamento” e garantir que o mock do ACP retornou sucesso.
Mini‑exemplo de teste Playwright:
// tests/e2e/gift-flow.spec.ts
import { test, expect } from '@playwright/test';
test('o usuário pode escolher um presente e ver os resultados', async ({ page }) => {
await page.goto('http://localhost:3000/widget');
await page.click('text=Presente de aniversário');
await page.click('text=Escolher');
await page.waitForSelector('[data-testid="gift-card"]');
const cards = await page.locator('[data-testid="gift-card"]').all();
expect(cards.length).toBeGreaterThan(0);
});
Num projeto real você adicionará:
- subir o npm run dev ou um test‑server separado no beforeAll do Playwright;
- mocks para MCP/ACP, para não tocar serviços de produção.
Mesmo um cenário simples assim já captura as “rachaduras” típicas entre o widget e o MCP: URL incorreta, erros de CORS, structuredContent inválido etc.
5. Testes smoke no CI: verificando se “isso ao menos sobe”
Falta a camada superior, a mais leve da pirâmide — os testes smoke. Eles não verificam todo o cenário como o E2E‑light, apenas respondem: o aplicativo está vivo e sobe antes do deploy?
Smoke vs E2E completo
Você já ouviu sobre o smoke‑test “manual” lá no segundo módulo: na época rodamos o primeiro “Hello GiftGenius”, verificamos que o widget renderizava, o ChatGPT o via e o botão abria um link. O objetivo era: garantir que Dev Mode + túnel + Apps SDK estavam configurados corretamente.
Agora a tarefa é parecida, só que automatizada e no CI:
- não tentamos simular todos os cenários do usuário;
- não conversamos com o ChatGPT real;
- apenas verificamos que:
- o app Next.js inicia;
- o servidor MCP responde ao menos a tools/list / tools/call;
- o endpoint ACP está vivo e retorna 200 para um JSON de teste.
Isso é especialmente importante antes do deploy em produção ou antes de enviar uma nova versão para a Store: é mais fácil pegar “tudo caiu e nem sobe” no CI do que descobrir pelos usuários.
Exemplo de teste smoke para MCP tools
Suponha que temos um módulo auxiliar que sobe o servidor MCP no teste ou usa um cliente MCP do SDK. Conceitualmente o teste fica assim:
// tests/smoke/mcp-tools.smoke.test.ts
import { createTestMcpClient } from './testClient';
test('O MCP responde a tools.list e tools.call(suggest_gifts)', async () => {
const client = await createTestMcpClient(); // sobe o servidor ou conecta a ele
const tools = await client.listTools();
expect(tools.some(t => t.name === 'suggest_gifts')).toBe(true);
const result = await client.callTool('suggest_gifts', {
occasion: 'birthday',
budget: { currency: 'USD', max: 50 },
});
expect(result.gifts.length).toBeGreaterThan(0);
});
Em análises mais profundas de testes de MCP, recomendam exatamente essa abordagem: usar um cliente MCP nos testes para verificar o ciclo completo do JSON‑RPC — list → call → resposta.
A implementação de createTestMcpClient pode ficar em utilitários: ela sobe o servidor no mesmo processo ou conecta a uma instância já em execução.
Teste smoke para ACP/checkout
De forma análoga, dá para escrever um teste simples para a camada de comércio, sem simular um pagamento real:
// tests/smoke/acp.smoke.test.ts
import fetch from 'node-fetch';
test('ACP test-intent retorna 200', async () => {
const res = await fetch('http://localhost:3000/api/acp/test-intent', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
amount: 10,
currency: 'USD',
}),
});
expect(res.ok).toBe(true);
});
Aqui não importa o que exatamente test-intent faz — ele pode apenas verificar acesso ao BD e retornar {"status":"ok"}. O principal é que o CI capture:
- uma env key esquecida;
- uma rota quebrada;
- parse de JSON com erro.
Pipeline mínimo de CI
Falaremos de CI/CD em detalhes nos módulos de deploy, mas um pipeline básico pode ser assim (no GitHub Actions):
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test # unit + contract
- run: npm run test:e2e # e2e/ui
- run: npm run test:smoke # smoke mcp/acp
Os comandos npm run test:e2e e npm run test:smoke já podem subir o dev‑server internamente, esperar até estar pronto e então rodar o Playwright / scripts em Node.
6. Mini‑mapa de testes para o GiftGenius
Para não se perder, vamos reunir tudo em uma tabela — o que testamos em cada nível e que pergunta isso responde.
| Nível | Exemplos para o GiftGenius | Ferramentas | Que pergunta responde |
|---|---|---|---|
| Unit | scoreGift, rankGifts, validadores de orçamento | Jest / Vitest | A lógica calcula corretamente? |
| Contract (schemas) | Schemas Zod Gift, SuggestGiftsResult, OrderCreated | Zod, AJV | Ainda falamos a mesma linguagem JSON com GPT/ACP? |
| UI/Component | Comportamento do widget ao clicar, chamada window.openai.callTool | React Testing Library | O UI dispara as ações corretas? |
| E2E‑light | O usuário percorreu o fluxo de escolha do presente e viu os cards | Playwright/Cypress | Todas as partes do GiftGenius se juntam em um fluxo funcional? |
| Smoke no CI | O MCP responde a tools.list/call, ACP test-intent 200 | Node‑scripts, MCP client | O aplicativo está vivo e conectado? |
Esse conjunto é o “mínimo viável de testes” para um ChatGPT App, como mencionado no plano do módulo: sem um time Enterprise de QA, mas com a garantia básica de que a produção não cai a cada espirro.
7. Erros típicos ao testar um ChatGPT App
Erro nº 1: tentar testar de forma determinística as respostas do modelo.
Às vezes devs tentam escrever testes do tipo “espero que o GPT responda com a string Aqui vão 5 ideias de presentes”. Esses testes são frágeis por definição: o modelo não é obrigado a repetir a formulação literalmente, e o próprio modelo pode ser atualizado. Neste módulo não tocamos o conteúdo das respostas — apenas verificamos que as ferramentas são chamadas, os schemas são válidos e o fluxo não quebra. Avaliar a qualidade dos textos é outra disciplina (M20, LLM‑evals).
Erro nº 2: falta de testes de contrato para os schemas do MCP.
É tentador descrever um schema Zod uma vez e esquecê‑lo. Depois você adiciona ao resultado da ferramenta o campo discount, atualiza o código, mas não atualiza o schema. O modelo continua enviando o formato antigo e seu código espera o campo novo — começam falhas estranhas em produção. Testes de contrato com Zod/JSON Schema evitam essas quebras “silenciosas”, portanto negligenciá‑los é um equívoco comum e doloroso.
Erro nº 3: tentar rodar E2E contra o chatgpt.com a partir do CI.
Alguns ainda tentam: executam Playwright contra o ChatGPT real, fazem login, clicam no UI — e recebem ban do Cloudflare, testes instáveis e possível violação dos termos de uso. O caminho certo é testar seu próprio host Next.js + MCP em isolamento, mockando window.openai e APIs externas, como recomendam os guias do Apps SDK e do MCP.
Erro nº 4: escrever apenas E2E e esquecer o nível unitário.
Às vezes vemos um projeto com um único E2E “enorme”, clicando por metade do app, e zero testes unitários. Essa abordagem dá uma falsa sensação de segurança: o teste ou está verde ou vermelho, mas localizar a causa é quase impossível, e cada execução leva minutos. Muito mais eficiente é ter dezenas de testes unitários rápidos para funções puras e alguns cenários E2E‑light bem escolhidos para caminhos críticos.
Erro nº 5: usar APIs externas reais nos testes comuns.
Stripe, catálogos externos, CRM — tudo isso é ótimo para testes de integração em ambiente controlado, mas não para o npm test do dia a dia. Se seus testes dependem de rede, rate limits de terceiros e servidores de produção de alguém, eles vão falhar por motivos alheios ao seu código. A melhor abordagem é mockar a API externa (com nock, msw etc.) e ter separadamente algumas verificações “reais” em um ambiente especial.
Erro nº 6: esquecer os testes smoke antes do deploy.
Você juntou uma feature, atualizou o MCP‑schema, ajustou o UI, clicou “Deploy” — e o Next.js não inicia porque alguém quebrou o next.config ou apagou o .env. Sem testes smoke automatizados, o CI deixa passar essas falhas óbvias para a produção. Uma suíte smoke simples que verifica “servidor subiu”, “MCP responde a uma chamada básica” e “endpoint de teste do ACP retorna 200” economiza horas de debug em produção e muitos nervos.
Erro nº 7: complicar demais o ambiente de testes no começo.
Às vezes, inspirados por melhores práticas de grandes empresas, queremos começar com dúzias de ambientes, testes de contrato complexos com geração de dados, cenários de carga etc. No fim, o time gasta semanas em infraestrutura e para de lançar features. Para começar com um ChatGPT App basta aquele “Sanity Suite” de que falamos: unit + contract + alguns E2E‑light + smoke no CI. Depois dá para evoluir conforme crescerem o tráfego e os requisitos.
GO TO FULL VERSION