Test-Driven Development (TDD) é uma metodologia de desenvolvimento na qual os testes são escritos antes da implementação do código. O desenvolvedor primeiro formula o comportamento esperado na forma de um teste com falha, depois escreve o código mínimo para passá-lo e, em seguida, refatora o resultado. De acordo com Martin Fowler (2023), TDD não é uma técnica de teste — é uma técnica de design que disciplina a arquitetura e reduz a quantidade de defeitos na fase de escrita do código.
Principais pontos
Test-Driven Development é uma prática de desenvolvimento de software na qual testes automatizados determinam a escrita do código de produção. Diferente da abordagem tradicional, onde o código é escrito e depois testado, o TDD inverte a sequência: primeiro o teste é escrito, depois o código que passa o teste.
O fundador do TDD é Kent Beck, que formulou essa prática no final dos anos 1990 como parte da metodologia Extreme Programming (XP). No livro “Test-Driven Development: By Example” (2002), Beck descreveu cinco regras do TDD que se tornaram canônicas: escreva o teste antes do código de produção, escreva exatamente o código necessário para passar no teste e refatore após cada ciclo.
O primeiro princípio — o teste define a interface. O desenvolvedor é forçado a pensar em como o componente será usado antes de pensar em como ele é implementado. Isso forma uma API limpa desde o início.
O segundo princípio — implementação mínima. Quando o teste é escrito, o desenvolvedor escreve exatamente o código de produção necessário para passá-lo — nem uma linha a mais. Isso evita abstração prematura e complexidade excessiva, que Martin Fowler chama de Speculative Generality.
A principal diferença entre TDD e teste “post-hoc” — a disciplina de sequência. No TDD, o teste não apenas verifica o código — ele guia sua estrutura. De acordo com um estudo da Microsoft Research (Nagappan et al., 2008), equipes que aplicam TDD demonstram uma redução de 40–90% na densidade de defeitos em comparação com equipes que usam a abordagem tradicional.
O ciclo Red-Green-Refactor é uma sequência de três etapas repetida para cada novo teste. Red: escrever um teste que não passa. Green: escrever o código mínimo para fazer o teste passar. Refactor: melhorar o código sem alterar seu comportamento.
O desenvolvedor escreve um teste que verifica uma funcionalidade ainda não implementada. Nesta fase, o teste deve falhar — isso confirma que o teste realmente está verificando algo. No ambiente de desenvolvimento Android, o framework JUnit 5 mostra um indicador vermelho para testes com falha, o que deu nome à fase.
class CalculatorTest {
fun testAddition() {
val result = Calculator().add(2, 3)
Assertions.assertEquals(5, result)
}
}
Nesta fase, o código de produção mínimo suficiente para passar no teste é escrito. Sem redundância — apenas o necessário para o indicador verde. Se a implementação pode ser uma constante, que seja uma constante. A refatoração ocorrerá na próxima etapa quando novos testes aparecerem.
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
}
O teste verde é um seguro para a refatoração. O desenvolvedor pode reescrever a implementação, otimizar o desempenho ou melhorar a legibilidade, confiante de que o teste detectará imediatamente qualquer desvio do comportamento esperado. No desenvolvimento móvel Android, esta fase é especialmente importante para extrair interfaces comuns e reduzir a duplicação de código.
A aplicação de TDD em projetos móveis traz benefícios mensuráveis, confirmados tanto por pesquisas acadêmicas quanto pela prática dos principais estúdios de desenvolvimento.
Um estudo da IBM (Bhat & Nagappan, 2006) em quatro projetos industriais mostrou que equipes que usam TDD produzem 40% menos defeitos em comparação com equipes similares que trabalham com a abordagem tradicional. Para o desenvolvimento móvel, onde o custo de corrigir um bug após o lançamento no Google Play é significativamente maior do que na fase de escrita do código, esta métrica é crítica.
Os testes escritos com TDD servem como documentação viva da API. Um desenvolvedor que ingressa no projeto pode ler os testes e entender como cada componente deve ser usado. Isso é especialmente valioso em situações de alta rotatividade da equipe — um desafio típico dos estúdios móveis.
A cobertura de código que excede 90% permite que os desenvolvedores refaturem sem medo de quebrar algo. O Google, em seu livro “Software Engineering at Google” (2020), chama a cobertura de testes de fator-chave para manter a base de código limpa em projetos com milhões de linhas de código.
O ecossistema TDD no desenvolvimento móvel inclui ferramentas para testes unitários, mocking e verificação de componentes de UI — tanto para Android quanto para iOS.
| Ferramenta | Plataforma | Finalidade |
|---|---|---|
| JUnit 5 | Android (Kotlin/Java) | Framework básico para testes unitários |
| Mockito | Android | Criação de objetos mock e verificação de chamadas |
| MockK | Android (Kotlin) | Mocking com sintaxe Kotlin-first e suporte a corrotinas |
| Turbine | Android | Teste de Kotlin Flow e fluxos reativos |
| XCTest | iOS (Swift) | Framework de teste padrão |
Para projetos Android em Kotlin, o stack padrão inclui JUnit 5 + MockK. MockK é preferível ao Mockito porque suporta recursos de primeira classe do Kotlin — classes seladas, corrotinas e funções suspend — sem configuração adicional.
No desenvolvimento iOS, o TDD é implementado através do XCTest — o framework integrado da Apple que fornece asserções, classes de teste e integração com CI/CD via Xcode Server ou GitHub Actions. Para mocking no iOS, são usadas as bibliotecas Cuckoo e OHHTTPStubs.
Vamos considerar um cenário real de TDD em Kotlin para Android — teste de um repositório de usuários. Primeiro escrevemos o teste, depois a implementação que o passa.
class UserRepositoryTest {
private val api = mockk<UserApi>()
private val dao = mockk<UserDao>()
private val repo = UserRepository(api, dao)
fun `when api returns user then cache and emit`() = runTest {
val user = User(1, "Alice")
coEvery { api.getUser(1) } returns user
every { dao.insert(user) } returns Unit
val result = repo.getUser(1)
assertEquals(user, result)
verify { dao.insert(user) }
}
}
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUser(id: Int): User {
val user = api.getUser(id)
dao.insert(user)
return user
}
}
Após passar o primeiro teste, adicionamos um segundo — verificando o comportamento durante um erro de rede. Agora o teste determina que quando a API falha, o repositório deve retornar dados do cache.
fun `when api fails then return cached user`() = runTest {
val cached = User(1, "Cached Alice")
coEvery { api.getUser(1) } throws IOException()
every { dao.getById(1) } returns cached
val result = repo.getUser(1)
assertEquals(cached, result)
}
A transição para TDD vem com erros típicos que podem anular todos os benefícios da metodologia. Entender essas armadilhas ajuda as equipes a adotar a prática de forma mais eficaz.
O primeiro e mais comum antipadrão — testar funcionalidade demais em um único teste. Um teste deve verificar exatamente uma afirmação. Se um teste falha, o desenvolvedor deve saber exatamente o que quebrou sem depuração adicional.
O segundo erro — escrever um teste que passa desde o início. Se o teste nunca foi vermelho pelo menos uma vez, não há certeza de que ele realmente verifica algo. Regra: nunca confie em um teste que você não viu falhar.
O terceiro erro comum — parar na fase verde. A refatoração não é opcional, mas uma etapa obrigatória do ciclo. Sem ela, a base de código degrada, os testes se tornam frágeis e os benefícios do TDD se perdem.
Perguntas frequentes
TDD é antes de tudo uma técnica de design, não de teste. Os testes no TDD desempenham o papel de especificação: eles definem a API do componente antes de sua implementação. O próprio Kent Beck chama o TDD de “uma disciplina de design, não de teste”.
De acordo com estudos da Microsoft Research, as equipes precisam de 3 a 6 meses de prática contínua para que o TDD se torne um hábito. Nas primeiras 2–3 semanas, a produtividade cai de 15–30%, mas após a adaptação retorna ao nível original ou o supera devido à redução do tempo de depuração.
Sim, mas com limitações. Para a lógica de UI (ViewModel, State), o TDD é diretamente aplicável. Para componentes visuais (Compose UI, SwiftUI Views), os testes de snapshot complementam o TDD, mas não o substituem. Recomenda-se separar a lógica de negócios da apresentação.
Para código legado, a estratégia recomendada são os “testes de caracterização” — onde os testes são escritos sobre o comportamento existente e, em seguida, o código é refatorado. Essa abordagem é descrita no livro de Michael Feathers “Working Effectively with Legacy Code” (2004) e permite introduzir o TDD gradualmente.
TDD e Clean Architecture se reforçam mutuamente. A arquitetura limpa exige limites claros entre as camadas, e o TDD força o desenvolvedor a projetar esses limites através de testes. A camada de domínio é testada isoladamente com dependências mock, e a camada de dados — através de testes de integração.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também