Pular para o conteúdo
Development

Fechando as lacunas de teste: lógica pura, um branch de mock inalcançável, e uma virada para Swift 6

Por Victor Da Luz
iosswifttestingswift-concurrencydev-logdeep-cut-atlas

Esse app foi renomeado depois para Deep Cut Atlas. Abaixo ele é chamado de “Discoverer” o tempo todo, porque foi assim que se chamava no dia em que isso aconteceu.

Essa era uma issue de limpeza vinda de um spike anterior de revisão do repositório, e acabou sendo mais interessante do que “adicionar uns testes” costuma ser. Três correções sem relação entre si, mas duas delas encontraram bugs reais que eu não estava procurando.

Problema

Três lacunas tinham se acumulado na suíte de testes do Discoverer. O algoritmo de agrupamento de álbuns da playlist e uma heurística de detecção de EP estavam ambos enterrados dentro de métodos de serviço que chamavam o MusicKit, então só podiam ser exercitados pelo caminho assíncrono completo de busca. O serviço mock tinha um parâmetro, libraryAlbumKeys, que ele ignorava silenciosamente - esse parâmetro é o que permite ao app avisar “você já possui esse álbum” em vez de “já está na sua playlist,” duas mensagens bem diferentes, e nenhum teste conseguia provar que esse branch funcionava. E o target de teste ainda compilava sob Swift 5, enquanto o target do app já era Swift 6 com isolamento padrão em Main Actor desde o primeiro dia. Ninguém nunca tinha checado se os testes sequer compilariam sob as mesmas regras que o app segue.

Por que essa abordagem

Para a primeira lacuna, a correção foi extração: tirar a parte pura do algoritmo (agrupar faixas por álbum, preservar a ordem de primeira aparição, recorrer ao título da faixa quando o título do álbum está faltando) para dentro da sua própria função, que recebe valores simples, não tipos do MusicKit. Mesmo movimento para a heurística de EP - assim que ela vira só (title, isCompilation, isSingle) -> RecordingType, você não precisa de uma chamada de rede para testar quatro branches de uma cadeia if/else.

Para o mock, eu não queria colar um “botão de teste” separado que pudesse se desalinhar da checagem real. Em vez disso, fiz o addAlbumToPlaylist do mock checar o mesmo parâmetro libraryAlbumKeys que o serviço real checa, na mesma ordem. Espelhar a lógica real em vez de forjar um atalho foi o que permitiu a próxima parte acontecer.

Implementação

A extração foi mecânica. A parte interessante foi o mock: no momento em que conectei o parâmetro ignorado, dois testes que antes passavam quebraram.

Descobri que dois dos álbuns de exemplo do mock - “Mordechai” e “Oncle Jazz” - faziam papel duplo. Estavam tanto na “biblioteca” falsa (então fetchLibraryAlbumKeys() retornava as chaves deles) QUANTO registrados como “já na playlist do usuário” para um cenário de teste completamente diferente. Essa sobreposição era invisível enquanto o mock nunca checava de fato a posse na biblioteca. No instante em que passou a checar, os dois álbuns ficaram genuinamente ambíguos: eles estão “já na sua biblioteca” ou “já na sua lista”?

Checei o que o serviço real faz exatamente nessa situação, e ele sempre checa a posse na biblioteca primeiro, incondicionalmente. Então a correção não era remendar em torno da ambiguidade - os testes é que estavam dependendo de um comportamento que o mock nunca deveria ter tido. Troquei as fixtures de “já na lista” por dois álbuns diferentes que não estão na biblioteca falsa, e agora os dois testes verificam algo que é de fato verdadeiro no código de produção.

A virada para Swift 6 foi quase anticlimática: mudei o target de teste para o modo de linguagem Swift 6 mais isolamento padrão em Main Actor, igualando o target do app, e rodei a suíte inteira de novo. Zero erros novos. Os 163 testes continuaram todos passando. Às vezes “simplesmente funciona” é o resultado real, e você só descobre isso virando a chave de verdade em vez de presumir.

Pegadinhas

Extrair a função de agrupamento para o próprio arquivo esbarrou num erro de isolamento do Swift 6 que eu já tinha visto uma vez antes, de uma forma diferente: uma struct simples sem anotação de actor herda o isolamento padrão em Main Actor do módulo, o que significa que seu inicializador e sua conformidade a Equatable sintetizados pelo compilador também ficam isolados na main actor. Uma função livre nonisolated não consegue chamar um inicializador isolado na main actor, mesmo para um tipo de valor inofensivo, sem nenhum estado mutável compartilhado de verdade. A correção é uma palavra só - marcar o tipo como nonisolated - mas você só descobre que precisa disso tentando compilar, e a mensagem de erro não aponta obviamente para “a struct precisa de uma anotação,” ela aponta para “uma conformidade está isolada na main actor,” o que soa estranho na primeira vez que você vê.

A segunda pegadinha foi autoinfligida: depois de renomear a fixture de “já na lista” de “Mordechai” (que por acaso é a faixa nº 1 na lista de fixtures) para um álbum diferente, que é a faixa nº 7, um teste travou com uma falha de force-unwrap. O tamanho de página padrão do teste só carrega as primeiras 5 faixas. Trocar de qual álbum um teste depende não é só uma substituição de string quando a fixture tem suposições posicionais embutidas - tive que aumentar o tamanho de página nos dois testes afetados.

Resultados e lições

163/163 testes passando, antes e depois da virada para Swift 6. A extração deu ao algoritmo de agrupamento de álbuns e à heurística de EP seus próprios testes unitários focados, totalmente independentes do MusicKit e do simulador. E conectar aquele único parâmetro de mock ignorado não só adicionou cobertura - revelou que dois testes existentes vinham passando pelo motivo errado.

Esse é o valor real do trabalho de “fechar lacunas de teste.” Cobrir mais linhas é a métrica de superfície; o ganho real é fazer a versão falsa do seu serviço se comportar o suficiente como a real para que uma regressão genuína não consiga se esconder atrás de uma coincidência conveniente de fixture.

Leitura relacionada

Você também pode achar útil

Proton

Proton Drive

Armazenamento em nuvem criptografado, da equipe por trás do Proton Mail.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais
Proton

Proton Mail

E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais