Construindo um player de música que eu não consigo ouvir
Esse app foi renomeado depois para Deep Cut Atlas. Abaixo ele aparece como “Discoverer”, porque era esse o nome no dia em que isso aconteceu.
A aba History do meu app de música mostra as faixas que você ouviu recentemente, e eu queria que você pudesse tocar em uma delas para dar play, um lembrete rápido, não um player completo. Só o suficiente para pensar “ah é, essa música”. Funcionalidade simples. Só tinha um problema: eu não consigo rodar isso.
O playback do MusicKit não funciona no Simulador do iOS. De jeito nenhum. Sem áudio, sem erro, só silêncio. A única forma de ouvir um segundo sequer do que estou construindo é colocar num iPhone físico com uma assinatura do Apple Music, e eu ainda não configurei a conta de desenvolvedor paga (isso é tarefa para depois). Então estou construindo um player de música que não tenho como escutar.
Isso parece motivo para não construir ainda. Mas “eu não consigo rodar os últimos 5%” não é motivo para pular os outros 95%, e a maior parte dessa funcionalidade não é o áudio, é a lista, a paginação, o tratamento do toque, o indicador de “tocando agora”, o fluxo de adicionar à playlist. Tudo isso eu consigo construir e verificar. O truque é traçar a linha no lugar certo.
Colocar a parte não testável atrás de uma portinha
O playback em si é quatro linhas:
let song = try await resolveSong(id: track.id)
player.queue = [song]
try await player.play()
// ...and player.pause()
Então escondi essas quatro linhas atrás de um protocolo com exatamente dois métodos:
@MainActor
protocol MusicPlaying: AnyObject {
func play(_ track: LibraryTrack) async throws
func pause()
}
A implementação real embrulha o ApplicationMusicPlayer. Uma implementação mock só registra o que foi pedido para tocar. No simulador o app injeta o mock; num dispositivo real, o de verdade. É o mesmo padrão que uso para toda a camada de biblioteca, então não era novidade, só significou que a superfície “não dá pra testar isso” encolheu de uma funcionalidade inteira para dois corpos de método.
O indicador é uma mentira que conto de propósito
Aqui está a parte que exigiu cuidado. Quando você toca numa linha, uma pequena forma de onda aparece para mostrar que está tocando. De onde vem isso?
Não vem do player. O player é um comando de disparar e esquecer, eu mando tocar, e ele toca (ou, no simulador, não toca). O indicador é controlado pelo próprio estado do view model: um nowPlayingTrackID e uma flag isPlaying que eu viro quando você toca. Estado otimista.
func togglePlayback(_ track: LibraryTrack) async {
if nowPlayingTrackID == track.id, isPlaying {
player.pause(); isPlaying = false; return
}
try? await player.play(track)
nowPlayingTrackID = track.id
isPlaying = true
}
Isso significa que o indicador funciona perfeitamente no simulador, toca numa linha, a forma de onda aparece, toca de novo, ela para, enquanto nenhum áudio toca de verdade. E essa é a armadilha que eu precisei nomear em voz alta quando revisei a funcionalidade: um indicador que alterna é evidência de que a interface está conectada, não evidência de que a música toca. São duas afirmações completamente diferentes, e a demonstração faz parecer que são uma só.
Escrevi isso de forma explícita para não me enganar depois: o simulador confirma os gestos, a lista, a paginação, os toasts. Se você realmente ouve o Khruangbin continua sem verificação até a funcionalidade rodar num telefone de verdade. Não “provavelmente está tudo bem”, sem verificação mesmo.
O que o estado otimista vai me custar depois
Existe um bug de verdade escondido nessa abordagem otimista, e estou deixando ele aí de propósito por enquanto. Como o indicador é controlado pela minha flag e não pelo player, ele não sabe quando uma música realmente termina. Num dispositivo, uma faixa vai acabar, o áudio vai parar, e minha forminha de onda vai continuar animando alegremente porque nada avisou o contrário.
Corrigir isso significa assinar o playbackStatus real do player e reconciliar meu estado com ele. Isso é trabalho genuinamente exclusivo de dispositivo, eu nem consigo escrever isso de forma significativa sem um dispositivo para observar, então entra no mesmo balde de “verificar o áudio”. Deixei uma nota na issue para o eu do futuro não redescobrir isso da forma difícil.
A lição
Quando parte de uma funcionalidade é genuinamente não testável no seu ambiente, o movimento certo não é pular ela nem fingir que o check verde cobre isso. É deixar essa parte o menor possível, construir e verificar tudo ao redor dela com honestidade, e ser preciso sobre o que o seu “funciona” realmente prova. Um mini-player que eu não consigo ouvir ainda é, na maior parte, construível, desde que eu não confunda a parte que testei com a parte que não testei.
Leitura relacionada
A playlist que já tinha o nome certo
Um artefato de renomeação, uma API sem campo de autor pra atualizar, e um teste em dispositivo real provando que o bug já tinha se corrigido sozinho, fechado como aceitar como está.
Álbuns em destaque, dados reais do MusicKit, e um teste que mentiu sobre estar passando
Não existe uma flag isEssential - o Essentials que você vê é uma playlist de músicas. featuredAlbums é o sinal real, e uma mudança de fixture expôs um teste que ninguém tinha rodado de novo.
A aba Discover que ficou permanentemente presa cavando
Uma busca de artista que falhava nunca contava como checada, então um artista ruim se reselecionava para sempre - até um teste de várias rodadas achar a segunda proteção.
Você também pode achar útil
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 maisNordPass
Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.
Como afiliado da NordPass, ganho com compras qualificadas.
Saiba maiseSIM Airalo
eSIM de dados local para viagens - sem necessidade de trocar um SIM físico.
Este é meu link de indicação da Airalo. Você recebe um desconto no seu primeiro eSIM e eu ganho crédito da Airalo para o meu.
Saiba mais