Como testar APIs com reconhecimento de localização com FastAPI e PostGIS

Detecte erros de ordem de coordenadas, erros de distância, casos de limite e suposições espaciais incorretas antes que cheguem à produção.
Um endpoint de localização pode retornar HTTP 200 e ainda assim estar errado em quilômetros. Essa é a parte desconfortável do teste de APIs geoespaciais: verificações comuns de API informam se a solicitação funcionou, e não se a geografia está correta.
Bugs espaciais podem parecer inofensivos na revisão de código. Latitude e longitude são invertidas. Um valor de raio é tratado como graus em vez de metros. Uma consulta funciona perto de uma cidade, mas se comporta de maneira diferente em outros lugares porque a coluna geométrica usa o tipo errado. O JSON é válido, os testes estão verdes e o mapa ainda está errado.
Neste tutorial, você construirá um pequeno endpoint FastAPI apoiado por PostGIS e então testará o comportamento espacial diretamente. Os exemplos usam acessórios determinísticos posicionados em distâncias conhecidas, de modo que os testes verificam a geografia e não apenas a estrutura da resposta.
O que estamos testando
A API expõe um endpoint: GET /places/nearby. Um cliente envia longitude, latitude e raio em metros. O serviço retorna locais próximos como recursos do GeoJSON, ordenados por distância.
A parte útil não é o endpoint em si. A parte útil é a estratégia de teste. Verificaremos a validação de coordenadas, os limites exatos da distância, a ordem das coordenadas GeoJSON e a consulta ao banco de dados que transforma um raio em um filtro espacial.
Arrume a mesa
Use uma coluna geográfica quando a API estiver respondendo a perguntas de distância em escala humana em metros. O PostGIS pode então calcular no elipsóide em vez de tratar a longitude e a latitude como coordenadas cartesianas planas.
CREATE EXTENSION IF NOT EXISTS postgis; CREATE TABLE places ( id bigserial PRIMARY KEY, name text NOT NULL, geog geography(Point, 4326) NOT NULL ); CREATE INDEX places_geog_gix ON places USING GIST (geog);Construa o ponto final
O endpoint abaixo valida longitude, latitude e raio com Pydantic e, em seguida, envia uma consulta parametrizada ao PostgreSQL. O SQL usa ST_DWithin para filtrar candidatos e ST_Distance para retornar a distância medida.
A API aceita lon e lat como parâmetros nomeados separados. Isso elimina a ambiguidade no limite HTTP. GeoJSON ainda usa (longitude, latitude) quando a resposta é criada.
from fastapi import FastAPI, Query from sqlalchemy import text app = FastAPI() @app.get('/places/nearby') async def nearby( lon: float = Query(ge=-180, le=180), lat: float = Query(ge=-90, le=90), radius_m: int = Query(gt=0, le=50000), ): sql = text(''' SELECT name, ST_X(geog::geometry) AS lon, ST_Y(geog::geometry) AS lat, ST_Distance( geog, ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography ) AS distance_m FROM places WHERE ST_DWithin( geog, ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography, :radius_m ) ORDER BY distance_m ''')Crie luminárias em distâncias conhecidas
Codificar duas empresas reais e esperar que a distância esteja aproximadamente correta é um teste frágil. Uma abordagem melhor é criar pontos de teste a distâncias exatas de uma origem fixa.
PostGIS pode gerar esses equipamentos com ST_Project. No banco de dados de teste, crie um ponto a 500 metros de distância, um a 950 metros de distância e outro a 1.500 metros de distância. Agora, uma consulta de 1.000 metros tem um resultado esperado inequívoco: os dois primeiros pertencem à resposta e o terceiro não.
WITH origin AS ( SELECT ST_SetSRID(ST_MakePoint(-121.4944, 38.5816), 4326)::geography AS g ) INSERT INTO places (name, geog) SELECT 'Cafe A', ST_Project(g, 500, radians(45)) FROM origin UNION ALL SELECT 'Cafe B', ST_Project(g, 950, radians(160)) FROM origin UNION ALL SELECT 'Cafe C', ST_Project(g, 1500, radians(300)) FROM origin;Teste o limite de 1 km
Este é o primeiro teste que realmente prova algo espacial. Não está verificando se o endpoint retorna uma lista. É verificar se o contrato de 1 quilômetro significa o que a API diz que significa.
def test_nearby_returns_only_points_inside_radius(client): response = client.get( '/places/nearby', params={ 'lon': -121.4944, 'lat': 38.5816, 'radius_m': 1000, }, ) assert response.status_code == 200 names = (f('properties')('name') for f in response.json()('features')) assert names == ('Cafe A', 'Cafe B') assert 'Cafe C' not in namesTeste a ordem das coordenadas antes de chegar ao PostGIS
As coordenadas GeoJSON são longitude primeiro e latitude depois. Muitos desenvolvedores naturalmente pensam primeiro na latitude, porque é assim que as coordenadas são comumente faladas. Uma camada de validação pode capturar a reversão mais óbvia antes que o banco de dados a veja.
A longitude deve estar entre -180 e 180. A latitude deve estar entre -90 e 90. Isso não detectará todas as inversões, porque ambos os números podem, às vezes, cair dentro de ambos os intervalos, mas detecta uma grande classe de falhas e fornece aos clientes um erro útil em vez de um mapa enganoso.
def test_rejects_invalid_latitude(client): response = client.get( '/places/nearby', params={'lon': 38.5816, 'lat': -121.4944, 'radius_m': 1000}, ) assert response.status_code == 422Teste a resposta como GeoJSON, não apenas JSON
Uma resposta pode ser JSON sintaticamente válida e ainda assim violar o contrato GeoJSON. Afirme o tipo de geometria e a ordem das coordenadas. Para um ponto de consulta conhecido, a longitude deve aparecer no elemento zero e a latitude no elemento um.
feature = response.json()('features')(0) assert feature('geometry')('type') == 'Point' lon, lat = feature('geometry')('coordinates') assert -180 <= lon <= 180 assert -90 <= lat <= 90Teste casos extremos ao redor do raio
As fronteiras espaciais merecem os seus próprios testes. Um local a 999 metros deve ser incluído em uma busca de 1.000 metros. Um lugar a 1.001 metros não deveria. Um local exatamente no limite deve se comportar de forma consistente com o predicado escolhido.
ST_DWithin inclui recursos cuja distância é menor ou igual ao limite fornecido, portanto, documente esse comportamento e teste-o explicitamente.
@pytest.mark.parametrize( 'distance_m, expected', ((999, True), (1000, True), (1001, False)), ) def test_radius_boundary(distance_m, expected): ...Não zombe do banco de dados
Os testes unitários são úteis para validação de solicitações, mas zombar da camada do repositório remove a parte mais importante desta API: o próprio PostGIS. Mantenha um pequeno banco de dados de testes de integração e execute pelo menos alguns testes na extensão real.
Isso detecta erros que as simulações não conseguem: comportamento geográfico versus geometria, incompatibilidades de SRID, índices espaciais, unidades e alterações nas funções SQL. Uma API de localização é um dos casos em que um teste barato de integração de banco de dados adquire muita confiança.
Verifique o desempenho com EXPLAIN
A correção é apenas metade da história. Um endpoint radius que varre todos os recursos funcionará no desenvolvimento e se tornará doloroso em escala de produção. Adicione um índice GiST e inspecione o plano de consulta.
O plano exato depende dos seus dados e da versão do PostgreSQL, mas você deverá ver o índice espacial participando da filtragem de candidatos. Trate uma varredura sequencial acidental em uma grande mesa de produção como uma regressão que vale a pena investigar.
EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM places WHERE ST_DWithin( geog, ST_SetSRID(ST_MakePoint(-121.4944, 38.5816), 4326)::geography, 1000 );O que eu colocaria no CI
Para um projeto real, eu dividiria os testes em dois grupos. Testes de validação rápidos são executados em cada commit. Os testes de integração PostGIS são executados em CI com um serviço PostgreSQL/PostGIS temporário.
A porta CI deve cobrir coordenadas inválidas, valores de raio zero ou negativos, acessórios de distância conhecida, comportamento de limite, saída GeoJSON e pelo menos um plano de consulta ou verificação de integridade de desempenho para conjuntos de dados maiores.
Isso é mais trabalhoso do que afirmar status_code == 200mas testa o que realmente interessa aos usuários: se a resposta da API é geograficamente verdadeira.
Conclusão
As APIs de localização falham de maneiras que os testes comuns da web não foram projetados para perceber. A resposta pode ter o esquema correto, a chamada ao banco de dados pode ser bem-sucedida e cada campo pode parecer razoável enquanto o local em si está errado.
A solução não é uma estrutura de testes gigante. Comece com alguns invariantes espaciais que você pode provar: distâncias conhecidas, intervalos de coordenadas válidos, ordem GeoJSON correta, comportamento determinístico de limites e testes reais de integração PostGIS. Essas verificações transformam a geografia de uma suposição em algo que seu pipeline de CI pode verificar.
Leitura adicional
- Documentação PostGIS: ST_DWithin, ST_Distance, ST_Project e tipos de geografia
- Documentação FastAPI: validação e teste de parâmetros de consulta
- RFC 7946: O formato GeoJSON
Compartilhe esta publicação
Ajude a levar esta informação adiante — publique no seu perfil.