Teste SQL

O objetivo deste artigo é demonstrar como transformar uma consulta SQL de estilo de entrevista em um fluxo de trabalho pronto para produção, testável e controlado por versão. Todos se concentram em escrever SQL que “funciona”, mas poucos testam se ele continua funcionando amanhã. Uma nova linha, uma hipótese alterada ou uma refatoração pode quebrar uma consulta silenciosamente. Este artigo percorre um fluxo de trabalho completo, mostrando como tratar SQL como software: versionado, testado e automatizado. Utilizaremos uma questão real de entrevista da Amazon sobre identificar clientes com o maior gasto diário. Em seguida, convertiremos o SQL em um componente testável, definiremos saídas esperadas e automatizaremos os testes com integração contínua e entrega contínua (CI/CD).

Nesta questão de entrevista da Amazon, foi solicitado encontrar os clientes com o maior gasto diário total de pedido entre uma determinada faixa de datas. Existem duas tabelas de dados neste projeto: clientes e pedidos. A tabela de clientes contém informações sobre os clientes, enquanto a tabela de pedidos contém informações sobre os pedidos realizados por esses clientes. Este problema é perfeito para ilustrar como o SQL pode ser tratado como software: a consulta deve ser correta, estável e resistente a regressões. A lógica se divide em três partes: identificar os pedidos realizados por cada cliente, calcular o gasto diário total de cada cliente e ordenar os clientes por gasto diário total.

A solução final em PostgreSQL é apresentada a seguir. O resultado esperado é uma lista de clientes com o maior gasto diário total de pedido. Neste estágio, a maioria das pessoas para. No entanto, o SQL quebra mais facilmente do que a maioria das pessoas pensa. Uma alteração no padrão, um nome de coluna renomeado ou uma nova fonte de dados pode introduzir erros silenciosos. Os testes protegem contra esses problemas. Existem três etapas de teste que abordaremos: converter a lógica em uma função, definir a saída esperada e escrever uma suíte de testes unitários.

Para testar o código SQL, começamos envolvendo-o em uma função Python usando um framework de teste leve como unittest. Em seguida, criamos um conjunto de dados de amostra controlado para testar. Também criamos a saída esperada, pois definir saídas esperadas cria um benchmark. Agora que temos a consulta definida, as entradas de teste e as saídas esperadas, podemos escrever um teste unitário real. A ideia é simples: executar a consulta com as entradas de teste e comparar o resultado com a saída esperada.

O framework de teste unittest do Python é altamente eficaz porque permite manter as dependências mínimas, enquanto fornece estrutura e repetibilidade. Começamos criando um banco de dados SQLite na memória. Isso garante que o teste seja executado em um ambiente isolado e não afete o banco de dados de produção. Em seguida, recriamos apenas as tabelas necessárias para a consulta. Embora a consulta só use um subconjunto de colunas, o esquema espelha uma tabela de produção realista. Isso reduz o risco de confiança falsa causada por esquemas oversimplificados. Em seguida, inserimos os dados de teste controlados definidos anteriormente.

Neste ponto, o banco de dados contém um estado conhecido e determinístico, o que é essencial para testes significativos. Antes de executar a consulta, carregamos e imprimimos as tabelas de teste usando Pandas. Embora essa etapa não seja estritamente necessária para automação, é muito útil durante o desenvolvimento e depuração. Quando um teste falha, poder inspecionar imediatamente os dados de entrada economiza muito tempo em comparação com verificar a lógica SQL, pois permite entender passo a passo o que o código está computando. Em seguida, executamos a consulta em teste e carregamos o resultado em um DataFrame, que fornece uma estrutura de dados conveniente para manipulação e análise.

Em seguida, devemos verificar os resultados linha por linha. A lógica de verificação faz uma afirmação manual entre a saída da consulta e o resultado esperado. A primeira verificação confirma se o número de linhas retornadas pela consulta corresponde ao que esperamos. Uma discrepância aqui indica imediatamente registros ausentes ou extras. Em seguida, iteramos pela saída esperada e comparamos com o resultado real da consulta linha por linha. Cada linha é verificada em todas as dimensões relevantes. Se qualquer valor difere do esperado, o teste é marcado como falho. Finalmente, o resultado do teste é resumido em uma mensagem de passagem/falha clara.

Este teste carrega algumas suposições dignas de nota. A estrutura completa do script, pronta para uso, pode ser vista aqui. Uma suíte de testes é útil apenas se for executada consistentemente sempre que necessário. Utilizamos CI/CD para automatizar os testes sempre que uma alteração de código for feita. Uma estrutura de repositório mínima pode parecer com isto. O próximo passo é garantir que esses testes sejam executados automaticamente sempre que o código for alterado. Para isso, usamos GitHub Actions. Essa ferramenta permite definir um fluxo de trabalho de CI que executa os testes SQL sempre que o código for enviado ou uma solicitação de pull for aberta.

Criamos o arquivo de fluxo de trabalho: no repositório, criamos a seguinte estrutura de pasta, se ela ainda não existir: .github/workflows/. Dentro dessa pasta, criamos um novo arquivo chamado test_sql.yml. O nome não é especial; o GitHub se importa apenas que o arquivo viva dentro do diretório .github/workflows/. Você pode nomeá-lo como quiser, mas test_sql.yml mantém as coisas claras e simples. Definimos quando o fluxo de trabalho deve ser executado. Aqui está o arquivo de fluxo de trabalho completo. Esta seção define quando o fluxo de trabalho é executado. Na prática, isso significa que o fluxo de trabalho será executado sempre que o código for enviado ou uma solicitação de pull for aberta. Isso ajuda