Saltar para o conteúdo principal
Odoo · Postmortem de engenharia

Os testes estavam a verde e o conector não funcionava: 32 falhas que a suite não via

Uma suite a verde demonstra que o seu código se comporta como o seu mock. Não demonstra que a API remota continua a ser a que pensava. Aqui está o caso, com números, e as provas que o teriam detetado.

Ecrã de Apps do Odoo com os módulos instalados e ativos num backend real

Se os seus testes do Odoo passam e o conector falha em produção, o motivo é quase sempre o mesmo: a suite não testa a API, testa o seu mock da API. O mock foi escrito por si, com base na documentação que existia nesse dia, e desde então não mudou. A API, sim. Um .get("shippingDate") cujo campo passou a ter outro nome não lança qualquer exceção: devolve None para sempre, e o teste que o cobre continua a verde porque o mock também diz shippingDate.

Ideia-chave: um teste contra um mock demonstra coerência interna, não conformidade externa. Só uma das duas o avisa quando o outro lado muda.

O caso: 236 de 236 a verde no Odoo 19, 18 e 17

Auditámos um conector de marketplace nosso antes de o pôr à frente de um vendedor real. A suite dava 236 de 236 a verde, e não em local: sobre builds reais do Odoo.sh nas três séries que suportamos.

Não funcionava. A auditoria encontrou 32 falhas reais por baixo do verde: 20 de contrato e 12 de versão. Nenhum dos 236 testes lhes tocava, porque nenhum olhava para fora do módulo.

O que estava realmente avariado: 20 falhas de contrato e 12 de versão

Uma falha de contrato é uma discrepância entre o que o módulo envia ou lê e o que a API declara. Surgiram três tipos:

  • Um parâmetro de consulta obrigatório que não era enviado. A API responde 400 e a falha é ruidosa… em produção.
  • Um parâmetro que a especificação não declara em nenhuma operação. Isso não dá erro: é ignorado. A resposta chega sem filtrar e parece que funcionou. Este é o perigoso.
  • Um campo de resposta lido com o nome errado. .get() devolve None e nada rebenta, nunca.

Quatro destas vinte não surgiram da leitura da especificação: surgiram ao chamar. O cabeçalho Link de paginação chegava relativo, e assim não era um URL válido: a partir da página um não se importava uma única encomenda. E um 403 por exceder a quota não vinha no formato de erro documentado, por isso era lido como uma falha de permissões: o conector marcava a conta como não autorizada e parava toda a sincronização.

Uma falha de versão é outra coisa: o mesmo código comporta-se de forma diferente consoante a série do Odoo. Estas surgiram do próprio módulo:

  • models.Constraint(...) existe no Odoo 19. No 18 e no 17 não é uma restrição: a classe carrega, não há qualquer erro, e a garantia de unicidade desapareceu.
  • res.groups escreve os seus membros users antes do Odoo 19 e user_ids a partir do 19. Se se enganar de lado, o módulo não instala.
  • Um ir.cron sem numbercall no Odoo 17 assume o valor por defeito 1: o cron corre uma vez e desativa-se sozinho. Ninguém vê um erro. Simplesmente deixa de sincronizar à terça-feira.
  • O Odoo 17 escreve <tree> onde o 18 e o 19 escrevem <list>, e rejeita uma vista cujo invisible lê um campo que a vista não apresenta: o 18 e o 19 toleram-no.

Repare no padrão: metade desta lista não dá erro. Dá silêncio. E o silêncio não é apanhado por nenhuma asserção que não o procure.

Porque é que a suite não viu: a anatomia de um teste que se testa a si próprio

Um teste típico de conector tem três peças: um mock que devolve um JSON, a chamada ao método e uma asserção sobre o resultado. Se esse JSON foi escrito a olhar para o seu próprio parser em vez de para a especificação do fornecedor, o teste é um espelho: passa sempre, e no dia em que a API mudar também passa.

Há uma segunda forma de verde vazio, e é específica do Odoo: a suite nem chegou a ser executada. No runner do Odoo.sh os testes post_install só correm numa instalação real: um -u, ou um -i sobre um módulo já instalado, executa zero testes e termina com código 0. E os seletores --test-tags "/modulo:Clase.metodo" não correspondem a nada aí; só funciona /modulo. Um pipeline mal escrito informa «sem falhas» porque não houve testes.

Aprendemo-lo com um dado incómodo e nosso: um dos nossos próprios verificadores de conformidade informava zero resultados por um bug seu. Resolvia apenas components/schemas, quando as respostas 200 dessa API apontam para components/responses, pelo que ignorava quase todos os endpoints. «Zero resultados» e «zero endpoints revistos» escrevem-se da mesma forma.

O segundo caso, pior: um reembolso duplo com a suite a verde

Segunda auditoria, outro conector, de uma plataforma de marketplaces. 163 de 163 a verde, também nas três séries e sobre builds reais. Por baixo: o registo de chamadas nunca tinha escrito uma linha, a sincronização incremental pedia a janela de datas errada, e um reembolso podia ser enviado duas vezes.

O do dinheiro é o que um mock não consegue reproduzir. O cliente HTTP repetia perante 5xx e perante timeout, e para qualquer método: também para o PUT que envia um reembolso. Um timeout não diz que o pedido não chegou, diz que não sabe se chegou; repetir aí uma escrita não idempotente é reembolsar duas vezes. E os redirecionamentos eram seguidos reescrevendo o método: um PUT de reembolso podia acabar num GET sem credenciais que devolvia 200 e era registado como sucesso. Nenhum teste o viu, e não foi descuido: um mock responde à primeira, sempre, sem timeouts, sem 502 e sem redirecionamentos. O ramo que causa dano é o que o mock nunca executa.

O das datas cabe numa palavra: as sincronizações incrementais pediam o criado desde a última passagem em vez do alterado. Uma encomenda criada de manhã e enviada à tarde não voltava a entrar na janela, e o seu estado no Odoo ficava congelado.

O do log é o mais silencioso dos três. O auxiliar que devolve o modelo de registo devolve um recordset, e quando tudo corre bem esse recordset está vazio; no Odoo, um recordset vazio é falso. A verificação era if not log_model: return False, por isso acertava sempre e todos os caminhos de «isto registamos» eram um no-op: nem exceção, nem rasto, nem linha. A prova de que estava assim desde o primeiro dia não veio de nenhum teste, mas de olhar em produção para o contador da sequência dessa tabela e encontrá-lo em NULL depois de meses de crons.

Os três tipos de prova que o teriam apanhado

1. Contrato contra a especificação real, não contra o seu mock

Não é preciso nenhum broker nem infraestrutura: basta ter o OpenAPI do fornecedor no repositório e compará-lo, estaticamente, com o seu cliente. Três portas que correm em segundos e terminam com código 1 se encontrarem algo:

  • Parâmetros de consulta: o que declara cada operação face ao que envia cada wrapper. Apanha o obrigatório em falta e o nome que ninguém declara.
  • Corpos de escrita: as chaves de primeiro nível do payload contra o esquema do pedido.
  • Parsers de resposta: cada nome de campo que o código lê contra o esquema da resposta.

A execução sobre esse conector: 87 wrappers mapeados contra 88 operações da especificação, 20 rotas de escrita com corpo JSON, 38 parsers sobre 38 operações distintas (5 funções multi-endpoint não analisáveis), 0 resultados. Esse zero significa alguma coisa porque leva o denominador impresso ao lado.

E quando o fornecedor disponibiliza sandbox, uma chamada real por família de endpoint: é a única prova que distingue «a especificação diz isto» de «o servidor faz isto».

2. Instalação real, não import (e depois, exercitar)

Que um módulo importe não diz nada; que compile, também não. A prova é instalá-lo: -i sobre uma base limpa e -i sobre uma base que já tinha a versão anterior. São duas provas distintas, e a segunda é a que rebenta com os dados noupdate e as migrações. Desenvolvemo-lo em um módulo que compila no Odoo 19 e mesmo assim não instala.

E depois de instalar, exercitá-lo. Passámos 47 apps do nosso catálogo por instalações limpas do Odoo 17 e 18 Community (instalar e depois chamar get_views, search_read e onchange): 61 de 94 execuções de versão saíram limpas. Das 33 restantes, a maioria nem chegava a instalar-se, quatro eram artefactos de testar cada app sem o seu módulo irmão, e duas instalavam perfeitamente e rebentavam ao serem exercitadas: um Expected singleton num compute de product.template e um modelo que assumia Enterprise numa base Community.

3. Verificação nas três versões, uma a uma

Um módulo que corre no 19, 18 e 17 são três módulos parecidos. A lista de armadilhas acima temo-la como uma verificação estática que corre num segundo sobre o port gerado, antes de gastar um build: cada entrada foi uma falha real e metade não falhava ruidosamente. É também o fundo do argumento quando alguém compara conector nativo com camada intermédia: a superfície de contrato não desaparece, só muda de dono. Tratámo-lo em Odoo 19 frente aos conectores de middleware.

O que mudar na segunda-feira

  • Guarde a especificação do fornecedor no repositório, versionada junto ao código que a consome. O seu diff é o seu alarme.
  • Ponha data e versão de contrato em cada fixture. Um mock que não diz contra o quê foi escrito já caducou e você não sabe.
  • Escreva um comparador estático entre o seu cliente e essa especificação. Duzentas linhas de Python que terminem com código diferente de zero.
  • Faça com que o pipeline falhe se o número de testes executados for zero, ou se descer face à execução anterior. «0 falhas» não significa nada; «0 falhas de 236», sim.
  • Instale a sério na CI (base limpa e base com a versão anterior) e exercite depois: abra as vistas e leia os registos pelo ORM. E se mantém várias séries do Odoo, corra a suite em todas.

E a mudança que realmente custa, que não é técnica: «a suite está a verde» não responde a «funciona?», responde a «o meu código continua a fazer o que eu pensava?». São duas perguntas, e confundi-las custou-nos 32 falhas num conector e um reembolso duplo latente noutro.

Se mantém conectores para clientes e quer uma segunda leitura sobre o que a sua suite não está a testar, é assim que trabalhamos com consultoras e equipas técnicas e com outros partners de Odoo.

Ligações úteis dentro da FlexigoTech

Trabalho com partners de OdooComo nos integramos com equipas que já têm o seu próprio ciclo de desenvolvimentoEngenharia de Odoo para consultorasCapacidade técnica para consultoras que precisam de conectores mantidosOdoo 19 frente aos conectores de middlewareOnde vive a superfície de contrato quando se mete uma camada intermédiaConector de Mirakl para OdooComo abordamos a sincronização de encomendas, devoluções e liquidações

Perguntas frequentes

Porque é que os meus testes do Odoo passam e o conector falha em produção?

Porque a suite testa o seu mock, não a API. O mock foi escrito contra a documentação desse dia e não mudou desde então; a API, sim. Um campo lido com o nome errado devolve None sem lançar exceção, por isso nenhuma asserção se apercebe. A única forma de o detetar é comparar o seu cliente com a especificação do fornecedor, ou fazer pelo menos uma chamada real ao sandbox.

O que é contract testing aplicado a um conector do Odoo?

Na sua versão prática, não precisa de broker nem de infraestrutura: consiste em guardar o OpenAPI do fornecedor no repositório e comparar estaticamente com ele os parâmetros que envia, as chaves dos corpos de escrita e os nomes de campo que cada parser lê. Três scripts que terminam com código diferente de zero. Cobrem a maioria das falhas de contrato antes de arrancar o Odoo.

Como sei se o meu mock está desatualizado face à API?

Três sinais: o fixture não indica contra que versão da especificação foi escrito; a especificação do fornecedor não está versionada no repositório, pelo que não pode ver o seu diff; e nenhum teste toca alguma vez na rede. Se se verificarem os três, o seu mock já está desatualizado, mesmo que a suite esteja a verde.

Basta testar o módulo no Odoo 19 se também o publico no 18 e no 17?

Não. Há construções que existem no 19 e que no 18 ou no 17 não falham: são ignoradas. models.Constraint no 18 ou no 17 não é uma restrição e não dá erro, pelo que perde a unicidade em silêncio. Um ir.cron sem numbercall no 17 desliga-se sozinho após a primeira execução. Ao passar 47 apps por instalações limpas do 17 e 18 Community, 61 de 94 execuções saíram limpas: um terço não.

Um teste de instalação é o mesmo que um teste de import?

Não, e confundi-los é dos erros mais caros. Importar o módulo só prova que o Python é válido. Instalá-lo carrega o XML, valida as vistas, aplica a segurança e executa as migrações. E mesmo instalado, é preciso exercitá-lo: no nosso próprio catálogo houve apps que instalavam perfeitamente e rebentavam ao abrir uma vista ou ler um registo pelo ORM.

O que fazemos sobre isto

Integrações à medidaO que faz, capturas, versões e preço.

Tem um conector com a suite a verde e duvida de que funcione?

Revemos a superfície de contrato contra a especificação real do fornecedor e a instalação nas séries do Odoo que suporta. Escreva para comercial@flexigobe.com ou ligue para +34 616 809 504.

Falar com um engenheiro