← Nesim Apps

Reescrever para Kotlin Multiplatform sem mudar o comportamento: o código antigo como oráculo de testes

Por Nesim Razon, desenvolvedor Android independente · 2026-11-16

#Notas de desenvolvimento#Android

Passei dois pequenos aplicativos leitores de RSS e um aplicativo do espaço para Kotlin Multiplatform, para que também rodassem no iOS. Isso significava reescrever código que dependia de classes do Android e do Java. Eu não queria que nenhum usuário atual notasse diferença, então deixei a implementação antiga nas fontes de teste e a fiz discutir com a nova. Foi assim.

O que precisou ser reescrito

O código comum do Kotlin não pode chamar classes do Android nem do Java, e o iOS não tem nenhuma delas. O analisador de RSS usava XmlPullParser, a limpeza de texto usava HtmlCompat, as datas passavam por SimpleDateFormat e o aplicativo do espaço embaralhava seus cartões diários com java.util.Random. As quatro coisas precisavam virar Kotlin simples que compila nas duas plataformas.

Uma reescrita assim é o lugar clássico para introduzir mudanças silenciosas: um título que perde um acento, uma data que se desloca uma hora, uma lista em outra ordem. Nada disso causa uma falha, então ninguém percebe até um usuário escrever uma avaliação.

Manter o código antigo como oráculo

Minha regra foi simples: não apagar a implementação antiga, movê-la. No TurkSail o analisador anterior foi, sem alterações, para um arquivo das fontes de teste do Android chamado LegacyRss. Ele não é mais publicado, mas ainda compila e ainda dá as respostas em que confio, porque usuários reais olham o resultado dele há anos.

O analisador novo fica no módulo compartilhado. O teste entrega o mesmo XML aos dois e pergunta se os resultados são iguais.

O teste de paridade

O teste analisa um feed com o analisador novo e com o antigo, e primeiro confere se ambos devolvem o mesmo número de artigos. Depois percorre os artigos um a um e compara seis coisas campo a campo: título, link, data, imagem de capa, resumo e HTML limpo. A mensagem de erro informa o feed, o índice do artigo e o campo, então a diferença é fácil de localizar.

Como o lado antigo precisa de classes do Android, o teste roda com Robolectric no módulo do aplicativo. Por padrão usa feeds de exemplo incluídos no projeto. Se eu o iniciar com -Drss.live=true, ele também baixa os feeds ao vivo dos sites reais e compara esses. Essa propriedade do sistema precisa ser repassada à JVM de testes na configuração de testes do módulo; caso contrário, a chave não faz nada e não avisa.

Armadilhas que o oráculo encontrou

Os feeds contêm HTML que, por sua vez, está escapado como XML, então as entidades são decodificadas duas vezes: uma na passada de XML e outra na passada de HTML. Um feed com um e comercial escapado seguido de uuml só vira a letra u com trema depois das duas passadas. Um atalho que decodifica uma única vez dá um texto errado.

O HtmlCompat conhecia as entidades HTML com nome e o código comum não. Escrevi uma tabela com as 96 entidades com nome do Latin-1, mais as poucas outras que os feeds usam, e trato entidades numéricas, decimais e hexadecimais, inclusive caracteres fora do plano básico. Entidades desconhecidas ficam como estão, que é o que o código antigo fazia.

As datas são RFC 822 com deslocamento, por exemplo dia da semana, dia, nome do mês, hora e um fuso com sinal de mais ou de menos e quatro dígitos. O SimpleDateFormat saiu, então analiso com uma expressão regular, monto o deslocamento à mão e converto com kotlinx-datetime. Os testes unitários fixam um carimbo de tempo concreto, com deslocamento e com GMT, e conferem que uma entrada inválida devolve null, como antes.

Um gerador aleatório que não pode mudar

O aplicativo do espaço mostra um cartão do dia. A ordem vem de embaralhar a lista de cartões com java.util.Random com semente 2026, e o índice do cartão é derivado da data. Se o embaralhamento mudasse, todos os usuários atuais veriam de repente outro cartão no mesmo dia.

Então escrevi uma classe pequena que reproduz o java.util.Random exatamente: o gerador congruencial linear de 48 bits, a forma como o nextInt escolhe um valor abaixo de um limite (com o laço de nova tentativa e o atalho para potências de dois) e o mesmo algoritmo que o Collections.shuffle usa. Aqui o oráculo é o original: um teste embaralha a lista com java.util.Random(2026) na JVM e confere que os identificadores saem na mesma ordem que com a minha classe.

Detalhes pequenos que importaram

O Android guarda o feed baixado em um arquivo de cache com nome derivado de um resumo do endereço. Mantive os nomes de arquivo idênticos, de modo que, depois da atualização, o aplicativo ainda encontra o arquivo antigo e o feed abre offline na hora. O teste de paridade não teria detectado uma mudança de nome de arquivo, por isso cito aqui.

A comparação ao vivo cobriu 7 categorias e 72 artigos no TurkSail e 20 artigos no MedyaFobi, com zero diferenças.

O que isso não prova

Um teste de paridade só cobre as entradas que você dá. Os feeds de exemplo e um instantâneo ao vivo não dizem nada sobre um artigo publicado na semana que vem com uma entidade ou um formato de data que nunca vi. Os feeds ao vivo também mudam, então uma execução verde hoje é uma afirmação sobre hoje.

Além disso, mostra igualdade, não correção: se o código antigo tinha um erro, o novo agora tem o mesmo erro de propósito. Aceitei isso porque o objetivo era manter o comportamento, e correções são uma mudança separada e visível. Com o gerador aleatório vale o mesmo limite: ele coincide para os tamanhos de lista que testei e confio em que o algoritmo é determinístico.

Perguntas frequentes

Por que manter o código antigo nos testes em vez de apagá-lo?

Porque é a descrição mais confiável do comportamento. Comparar o código novo com ele campo a campo é mais rápido e seguro do que escrever os valores esperados à mão.

O teste de paridade precisa de acesso à internet?

Não por padrão. Ele usa feeds de exemplo incluídos no projeto. A comparação ao vivo só roda se o teste for iniciado com -Drss.live=true.

Por que reimplementar o java.util.Random em vez de usar outro embaralhamento?

Outro embaralhamento mudaria o cartão do dia de todos os usuários atuais. Reproduzir o gerador exato mantém a sequência inalterada.

Um teste de paridade verde garante que não há regressões?

Não. Ele só cobre as entradas que vê e os dados ao vivo mudam; reduz o risco, mas não o elimina.

Mais do blog