Testando Seu App
Testando Seu App
O Turn Lua SDK fornece um ambiente de teste completo usando lester, um framework de teste BDD em Lua puro que funciona com Luerl.
Por que lester?
Luerl implementa Lua 5.3 na VM BEAM do Erlang e não pode carregar módulos baseados em C. Muitos frameworks de teste Lua populares, como Busted, dependem de módulos C (como LuaFileSystem), tornando-os incompatíveis com Luerl. lester é escrito em Lua puro sem dependências C e fornece uma API compatível com Busted (describe, it, before, after), garantindo que seus testes sejam executados no mesmo ambiente da produção.
Desenvolvimento Orientado a Testes (TDD) Local
O Docker SDK facilita fluxos de trabalho TDD:
# Executar testes uma vez
make test
# Modo watch - executa testes automaticamente quando arquivos mudam
make watch
# Ou use turn-app diretamente
turn-app test # Executar testes
turn-app watch # Modo watch
O app gerado inclui uma suíte de testes completa que você pode executar imediatamente. Todos os testes são executados no mesmo ambiente Luerl que seu app usará em produção, garantindo confiança no seu código.
Escrevendo Testes
Crie arquivos de teste usando a API estilo BDD do lester:
-- test_my_app.lua
local lester = require('lester')
local describe, it, before = lester.describe, lester.it, lester.before
-- Mock da biblioteca turn
local turn = {
app = { ... },
logger = { ... }
}
package.loaded["turn"] = turn
local App = require("my_app")
-- Suíte de Teste
describe("My App", function()
before(function()
-- Código de configuração executado antes de cada teste
end)
it("deve lidar com evento de instalação", function()
local result = App.on_event(
{ uuid = "test-uuid" },
{ vname = "+27123456789" },
"install",
{}
)
assert(result == true, "Esperado que install retorne true")
end)
end)
-- Executar os testes
lester.report()
local exit_code = lester.exit()
return exit_code
Executando Testes
Se você está usando o Docker SDK (recomendado), os testes já estão configurados:
# Executar testes uma vez
make test
# Modo watch para fluxo de trabalho TDD
make watch
Para usuários do template tradicional, use o helper Make:
# Da raiz do seu projeto
make test-lua-app APP=my_app
lester fornece organização de testes estilo BDD, saída colorida e mensagens detalhadas de falha mostrando quais testes passaram ou falharam.
Padrões e Melhores Práticas de Testes
Padrão 1: Simulando APIs Externas
Ao seu app fazer chamadas para APIs externas, sempre simule-as em testes.
describe("integração com API de pagamento", function()
before(function()
turn.test.reset()
end)
it("deve processar pagamento com sucesso", function()
-- Simular a resposta da API
turn.test.mock_http(
{
method = "POST",
url = "https://api.stripe.com/v1/charges",
body = json.encode({
amount = 1000,
currency = "usd",
source = "tok_visa"
})
},
{
status = 200,
body = json.encode({
id = "ch_123",
status = "succeeded",
amount = 1000
})
}
)
-- Acionar o manipulador de eventos
local result = app:on_event({}, number, "process_payment", {
amount = 1000,
currency = "usd",
token = "tok_visa"
})
-- Verificar resultado
assert(result.success == true)
assert(result.charge_id == "ch_123")
-- Verificar que a API foi chamada corretamente
turn.test.assert_http_called({
method = "POST",
url = "https://api.stripe.com/v1/charges"
})
end)
it("deve lidar com falhas de pagamento", function()
-- Simular falha da API
turn.test.mock_http(
{method = "POST", url = "https://api.stripe.com/v1/charges"},
{
status = 402,
body = json.encode({
error = {
type = "card_error",
message = "Seu cartão foi recusado"
}
})
}
)
local result = app:on_event({}, number, "process_payment", {
amount = 1000,
currency = "usd",
token = "tok_visa_debit"
})
-- Verificar tratamento de erros
assert(result.success == false)
assert(result.error:match("recusado"))
end)
end)
Padrão 2: Testando Tratamento de Erros
Sempre teste tanto os caminhos de sucesso quanto de erro.
describe("busca de dados de usuário", function()
before(function()
turn.test.reset()
end)
it("deve retornar dados do usuário quando encontrado", function()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/users/123"},
{status = 200, body = json.encode({id = 123, name = "João Silva"})}
)
local result = app:on_event({}, number, "get_user", {user_id = 123})
assert(result.user.name == "João Silva")
end)
it("deve lidar com usuário não encontrado", function()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/users/999"},
{status = 404, body = json.encode({error = "Usuário não encontrado"})}
)
local result = app:on_event({}, number, "get_user", {user_id = 999})
assert(result.error ~= nil)
assert(result.error:match("não encontrado"))
end)
it("deve lidar com timeout de rede", function()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/users/123"},
{status = 0, error = "Tempo limite de requisição esgotado"} -- Simular timeout
)
local result = app:on_event({}, number, "get_user", {user_id = 123})
assert(result.error ~= nil)
assert(result.error:match("tempo limite"))
end)
end)
Padrão 3: Testando Operações Assíncronas com Leases
describe("processamento de lote assíncrono", function()
before(function()
turn.test.reset()
end)
it("deve adquirir lease e processar lote", function()
local lease_id = nil
local lease_released = false
-- Simular aquisição de lease
turn.test.mock_lease_acquire("batch_process", function(id)
lease_id = id
return true
end)
-- Simular chamada de API dentro da operação de lease
turn.test.mock_http(
{method = "POST", url = "https://api.example.com/batch"},
{status = 200, body = json.encode({processed = 100})}
)
-- Acionar processamento de lote
local result = app:on_event({}, number, "process_batch", {
items = {"item1", "item2", "item3"}
})
-- Verificar que o lease foi adquirido
assert(lease_id ~= nil, "Lease deveria ter sido adquirido")
-- Verificar que o processamento ocorreu
assert(result.status == "processing")
-- Simular conclusão e liberação de lease
turn.test.assert_lease_released(lease_id)
end)
it("deve lidar com falha de aquisição de lease", function()
-- Simular falha de lease
turn.test.mock_lease_acquire("batch_process", function()
return false -- Falha ao adquirir
end)
local result = app:on_event({}, number, "process_batch", {
items = {"item1", "item2"}
})
-- Deve retornar erro quando não consegue adquirir lease
assert(result.error ~= nil)
assert(result.error:match("lease"))
end)
end)
Padrão 4: Testando Comunicação entre Apps
describe("integração entre apps", function()
before(function()
turn.test.reset()
end)
it("deve verificar saldo com app de pagamento", function()
-- Simular resposta do app de pagamento
turn.test.mock_app_call(
"payment_app",
"check_balance",
{
balance = 5000,
currency = "USD",
available = true
}
)
-- Seu app verifica saldo
local result = app:on_event({}, number, "verify_funds", {
account_id = "acc_123",
required_amount = 1000
})
-- Verificar que chamou o app de pagamento
turn.test.assert_app_called(
"payment_app",
"check_balance",
{account_id = "acc_123"}
)
-- Verificar resultado
assert(result.sufficient_funds == true)
assert(result.balance == 5000)
end)
it("deve lidar com app de pagamento indisponível", function()
-- Simular app de pagamento retornando erro
turn.test.mock_app_call(
"payment_app",
"check_balance",
{
error = "Serviço temporariamente indisponível"
}
)
local result = app:on_event({}, number, "verify_funds", {
account_id = "acc_123",
required_amount = 1000
})
-- Deve lidar com erro graciosamente
assert(result.error ~= nil)
assert(result.error:match("indisponível"))
end)
end)
Padrão 5: Testando Configuração
describe("comportamento baseado em configuração", function()
before(function()
turn.test.reset()
end)
it("deve usar URL de produção quando configurado", function()
turn.test.set_config("environment", "production")
turn.test.set_config("api_url", "https://api.production.com")
turn.test.mock_http(
{method = "GET", url = "https://api.production.com/data"},
{status = 200, body = "{}"}
)
app:on_event({}, number, "fetch_data", {})
turn.test.assert_http_called({
method = "GET",
url = "https://api.production.com/data"
})
end)
it("deve usar URL de staging quando configurado", function()
turn.test.set_config("environment", "staging")
turn.test.set_config("api_url", "https://api.staging.com")
turn.test.mock_http(
{method = "GET", url = "https://api.staging.com/data"},
{status = 200, body = "{}"}
)
app:on_event({}, number, "fetch_data", {})
turn.test.assert_http_called({
method = "GET",
url = "https://api.staging.com/data"
})
end)
it("deve falhar quando configuração ausente", function()
turn.test.set_config("api_url", nil) -- Sem configuração
local result = app:on_event({}, number, "fetch_data", {})
assert(result.error ~= nil)
assert(result.error:match("configuração"))
end)
end)
Exemplo de Teste Completo
Aqui está um exemplo completo mostrando vários padrões de teste juntos:
local lester = require("lester")
local describe, it, before = lester.describe, lester.it, lester.before
-- Carregar seu app
local App = require("my_payment_app")
local json = require("json")
-- Configurar número de teste e contexto de app
local number = {id = "test-number-123"}
local app = App
describe("MyPaymentApp", function()
before(function()
-- Redefinir estado antes de cada teste
turn.test.reset()
-- Configurar configuração de teste
turn.test.set_config("stripe_api_key", "sk_test_123")
turn.test.set_config("webhook_secret", "whsec_test_456")
end)
describe("instalação", function()
it("deve criar campos de contato personalizados", function()
local result = app:on_event({}, number, "install", {})
assert(result.success == true, "Instalação deveria ter sucesso")
-- Verificar que campos de contato foram criados
local contact = turn.test.get_contact("contact-1")
assert(contact.fields ~= nil, "Contato deveria ter campos personalizados")
end)
end)
describe("processamento de pagamento", function()
it("deve processar pagamento com cartão válido", function()
-- Simular resposta da API Stripe
turn.test.mock_http(
{
method = "POST",
url = "https://api.stripe.com/v1/payment_intents",
body = json.encode({
amount = 2000,
currency = "usd",
payment_method = "pm_card_visa"
})
},
{
status = 200,
body = json.encode({
id = "pi_123",
status = "succeeded",
amount = 2000,
currency = "usd"
})
}
)
local contact = turn.test.get_contact("contact-1")
local result = app:on_event({}, number, "journey_event", {
contact = contact,
card = {
name = "process_payment",
data = {
amount = 2000,
currency = "usd",
payment_method = "pm_card_visa"
}
}
})
-- Verificar resultado
assert(result.flow == "continue", "Deveria continuar o fluxo")
assert(result.data.payment_status == "succeeded")
assert(result.data.payment_id == "pi_123")
-- Verificar que a API Stripe foi chamada
turn.test.assert_http_called({
method = "POST",
url = "https://api.stripe.com/v1/payment_intents"
})
end)
it("deve lidar com cartão recusado", function()
turn.test.mock_http(
{method = "POST", url = "https://api.stripe.com/v1/payment_intents"},
{
status = 402,
body = json.encode({
error = {
type = "card_error",
code = "card_declined",
message = "Seu cartão foi recusado."
}
})
}
)
local contact = turn.test.get_contact("contact-1")
local result = app:on_event({}, number, "journey_event", {
contact = contact,
card = {
name = "process_payment",
data = {amount = 2000, currency = "usd"}
}
})
assert(result.flow == "continue")
assert(result.data.payment_status == "failed")
assert(result.data.error:match("recusado"))
end)
it("deve lidar com erro de rede", function()
turn.test.mock_http(
{method = "POST", url = "https://api.stripe.com/v1/payment_intents"},
{status = 0, error = "Erro de conexão"}
)
local contact = turn.test.get_contact("contact-1")
local result = app:on_event({}, number, "journey_event", {
contact = contact,
card = {name = "process_payment", data = {amount = 2000}}
})
assert(result.flow == "continue")
assert(result.data.payment_status == "error")
assert(result.data.error:match("conexão"))
end)
end)
describe("webhooks", function()
it("deve verificar assinatura do webhook", function()
local webhook_payload = json.encode({
type = "payment_intent.succeeded",
data = {object = {id = "pi_123", status = "succeeded"}}
})
local result = app:on_event({}, number, "http_request", {
method = "POST",
path = "/webhook",
headers = {
["stripe-signature"] = "t=1234567890,v1=valid_signature"
},
body = webhook_payload
})
assert(result.status == 200)
assert(result.body:match("sucesso"))
end)
it("deve rejeitar assinatura inválida", function()
local result = app:on_event({}, number, "http_request", {
method = "POST",
path = "/webhook",
headers = {
["stripe-signature"] = "t=1234567890,v1=invalid_signature"
},
body = "{}"
})
assert(result.status == 401)
assert(result.body:match("não autorizado"))
end)
end)
describe("integração com app de notificação", function()
it("deve notificar usuário sobre pagamento bem-sucedido", function()
-- Simular app de notificação
turn.test.mock_app_call(
"notification_app",
"send_notification",
{success = true, message_id = "msg_789"}
)
local contact = turn.test.get_contact("contact-1")
app:on_event({}, number, "notify_payment_success", {
contact = contact,
payment_id = "pi_123",
amount = 2000
})
-- Verificar que app de notificação foi chamado
turn.test.assert_app_called(
"notification_app",
"send_notification",
{
contact_id = contact.id,
message = "Seu pagamento de $20.00 foi bem-sucedido!"
}
)
end)
end)
describe("manifest", function()
it("deve ter manifest.json válido", function()
local manifest = turn.test.load_manifest()
assert(manifest ~= nil, "Manifest deveria carregar")
assert(manifest.app.name == "my_payment_app")
assert(manifest.app.version ~= nil)
-- Verificar campos obrigatórios
turn.test.assert_manifest_has("app.name")
turn.test.assert_manifest_has("app.version")
turn.test.assert_manifest_has("app.description")
end)
it("deve validar estrutura do manifest", function()
local valid, errors = turn.test.validate_manifest()
assert(valid, "Validação do manifest falhou: " .. json.encode(errors or {}))
end)
end)
end)
-- Executar testes
lester.report()
local failures = lester.count_failures()
os.exit(failures > 0 and 1 or 0)
Depurando Falhas de Teste
Quando os testes falharem, aqui estão técnicas de depuração úteis:
1. Inspecionar Requisições HTTP
after(function()
local requests = turn.test.get_http_requests()
print("\n=== Requisições HTTP Feitas ===")
for i, req in ipairs(requests) do
print(string.format("%d. %s %s", i, req.method, req.url))
if req.body then
print(" Corpo:", req.body)
end
end
end)
2. Verificar Chamadas de App
after(function()
local calls = turn.test.get_app_calls()
print("\n=== Chamadas de App Feitas ===")
for i, call in ipairs(calls) do
print(string.format("%d. %s.%s", i, call.app, call.event))
print(" Dados:", json.encode(call.data))
end
end)
3. Verificar Dados de Teste
it("deve ter dados de teste corretos", function()
turn.test.reset()
local contact = turn.test.get_contact("contact-1")
print("Contato de teste:", json.encode(contact))
local journey = turn.test.get_journey("journey-1")
print("Jornada de teste:", json.encode(journey))
end)
4. Usar Asserções Descritivas
-- ❌ RUIM: Mensagem de erro não clara
assert(result.status == "success")
-- ✅ BOM: Mensagem de erro clara
assert(
result.status == "success",
string.format("Esperava status 'success' mas obteve '%s'", result.status)
)
Armadilhas Comuns de Testes
Armadilha 1: Esquecer de Redefinir Estado
-- ❌ RUIM: Estado vaza entre testes
it("teste 1", function()
turn.test.mock_http({...}, {...})
-- lógica de teste
end)
it("teste 2", function()
-- Ainda tem simulação do teste 1!
end)
-- ✅ BOM: Redefinir antes de cada teste
before(function()
turn.test.reset()
end)
Armadilha 2: Não Simular Todas as Chamadas HTTP
-- ❌ RUIM: Esqueceu de simular segunda chamada de API
it("deve fazer duas chamadas de API", function()
turn.test.reset()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/data"},
{status = 200, body = "{}"}
)
-- Este código faz DUAS chamadas HTTP mas apenas uma está simulada
app:on_event({}, number, "fetch_all_data", {})
-- Segunda chamada HTTP falhará!
end)
-- ✅ BOM: Simular ambas as chamadas
it("deve fazer duas chamadas de API", function()
turn.test.reset()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/users"},
{status = 200, body = json.encode({users = []})}
)
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/orders"},
{status = 200, body = json.encode({orders = []})}
)
app:on_event({}, number, "fetch_all_data", {})
end)
Armadilha 3: Não Testar Caminhos de Erro
-- ❌ RUIM: Apenas testa o caminho feliz
describe("busca de usuário", function()
it("deve retornar usuário", function()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/user/1"},
{status = 200, body = json.encode({id = 1, name = "João"})}
)
local result = app:on_event({}, number, "get_user", {id = 1})
assert(result.user.name == "João")
end)
end)
-- ✅ BOM: Testa tanto sucesso quanto caminhos de erro
describe("busca de usuário", function()
before(function()
turn.test.reset()
end)
it("deve retornar usuário quando encontrado", function()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/user/1"},
{status = 200, body = json.encode({id = 1, name = "João"})}
)
local result = app:on_event({}, number, "get_user", {id = 1})
assert(result.user.name == "João")
end)
it("deve lidar com usuário não encontrado", function()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/user/999"},
{status = 404, body = json.encode({error = "Não encontrado"})}
)
local result = app:on_event({}, number, "get_user", {id = 999})
assert(result.error ~= nil)
end)
it("deve lidar com erro de rede", function()
turn.test.mock_http(
{method = "GET", url = "https://api.example.com/user/1"},
{status = 0, error = "Falha de conexão"}
)
local result = app:on_event({}, number, "get_user", {id = 1})
assert(result.error ~= nil)
end)
end)