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()devolveNonee 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.groupsescreve os seus membrosusersantes do Odoo 19 euser_idsa partir do 19. Se se enganar de lado, o módulo não instala.- Um
ir.cronsemnumbercallno 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 cujoinvisiblelê 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
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
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.

