Teste unitário é um método de verificação de software onde a correção de módulos individuais ou funções do código é testada isoladamente do resto do sistema. De acordo com Martin Fowler, 2026, os testes unitários são a base de CI/CD e refatoração, fornecendo feedback rápido sobre o funcionamento do código. Teste modular ajuda a detetar erros nas fases iniciais do desenvolvimento, reduzindo o custo da sua correção dezenas de vezes.
Principais pontos
Teste unitário é o processo de verificar unidades individuais do código-fonte — funções, métodos, classes — isoladamente do resto do programa. Cada teste executa um cenário de uso específico do módulo e verifica se o resultado corresponde ao esperado. Os testes unitários são escritos na mesma linguagem de programação que o código principal e executados automaticamente no ambiente de desenvolvimento ou num pipeline CI/CD. Ao contrário dos testes de integração, os testes unitários não interagem com bases de dados reais, sistemas de ficheiros ou serviços de rede.
O objetivo principal é o feedback rápido sobre a correção do código após alterações. Se um desenvolvedor refatora um método, o conjunto de testes unitários confirma que o comportamento não foi quebrado. De acordo com Google Testing Blog (2025), projetos com cobertura de testes unitários acima de 60% têm 2,5 vezes menos incidentes em produção. Benefícios adicionais: documentação do código (os testes mostram como usar a API), simplificação da refatoração (a implementação pode ser alterada mantendo o comportamento) e diagnóstico rápido de regressões.
Nem todo teste automatizado é um teste unitário. Critérios: um único módulo (classe ou função) é testado, as dependências externas são substituídas por mocks ou stubs, o teste é executado em milissegundos e não requer iniciar um servidor ou base de dados. Um teste que acede a uma base de dados real é um teste de integração. Um teste que abre um navegador é um teste E2E. Compreender os limites entre os tipos de testes é importante para distribuir corretamente os esforços na pirâmide de testes.
Testes unitários de qualidade seguem os princípios FIRST formulados por Robert C. Martin. Cada teste deve ser Fast (rápido — milissegundos), Isolated (isolado — não depende de outros testes), Repeatable (repetível — mesmo resultado em qualquer máquina), Self-validating (autovalidável — resultado "passed" ou "failed", sem verificação manual) e Timely (oportuno — escrito antes ou simultaneamente com o código). Violar qualquer princípio reduz o valor do teste.
Um modelo padrão para escrever testes unitários. Arrange — preparação de dados e dependências: criar objetos, configurar mocks, definir parâmetros de entrada. Act — execução da ação testada: chamar um método ou função. Assert — verificação do resultado: comparar o valor real com o esperado. A divisão em três blocos torna o teste legível e compreensível. Se o bloco Assert requer lógica complexa, o teste provavelmente está a verificar demasiado de uma só vez.
// Exemplo de teste unitário com padrão AAA em Kotlin com JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — criar o objeto de teste
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — executar a ação
val result = calculator.add(2, 3)
// ASSERT — verificar o resultado
Assertions.assertEquals(5, result)
}
}
O nome do teste deve descrever o que está a ser verificado e qual o resultado esperado. Formato: [methodName]_[scenario]_[expectedResult]. Exemplo: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Um bom nome de teste substitui um comentário e, ao falhar, indica imediatamente qual funcionalidade foi quebrada. Evite nomes como test1, checkSomething ou verify — não trazem informação e dificultam o diagnóstico.
Para isolar o módulo testado das dependências externas, são utilizados duplos de teste (test doubles). Tipos principais: mocks — verificam que um método específico foi chamado com os parâmetros esperados; stubs — retornam valores predefinidos quando um método é chamado; fakes — implementações simplificadas de componentes reais (por exemplo, InMemoryUserRepository em vez de UserRepository que trabalha com uma BD). A escolha depende do que precisa ser verificado: estado (stub) ou interação (mock).
| Duplo | O que verifica | Exemplo |
|---|---|---|
| Mock | Chamada de método com parâmetros corretos | userRepository.save(user) foi chamado exatamente 1 vez |
| Stub | Valor de retorno | repository.findById(1) retorna User(id=1, name="Test") |
| Fake | Lógica através de implementação simplificada | InMemoryMapUserRepository com HashMap em vez de BD |
| Spy | Mocking parcial de um objeto real | spy(repo).when(findById).thenReturn(user) |
Mockito é o framework de mocking mais popular para Java e Kotlin. Permite criar mocks através de mock(), configurar valores de retorno através de when().thenReturn() e verificar chamadas através de verify(). Versões modernas do Mockito (5.x) suportam mocks estáticos (mockStatic) e sintaxe simplificada através de BDDMockito (given-willReturn). Uma regra importante: não mocke o que não é seu — não crie mocks para objetos de valor e bibliotecas padrão.
// Exemplo de teste unitário com Mockito em Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) é uma metodologia onde o teste é escrito antes da implementação do código. O ciclo "Red-Green-Refactor": escrever um teste que falha (Red), escrever o código mínimo para passar o teste (Green), melhorar o código sem alterar o comportamento (Refactor). TDD garante que todo o código está coberto por testes (cobertura = 100% para a funcionalidade implementada) e que o código é testável — se o código é difícil de testar, a arquitetura precisa de melhoria.
De acordo com a pesquisa da IBM (2006-2026, estudo longitudinal), as equipas que usam TDD têm 40-80% menos defeitos em produção em comparação com equipas que escrevem testes após o código. TDD também melhora a arquitetura: o desenvolvedor é forçado a pensar no design da API antes da implementação, levando a baixo acoplamento (loose coupling) e alta coesão (high cohesion). Um efeito adicional é a documentação com código vivo: os testes servem como especificação do comportamento do módulo, que está sempre atualizada.
TDD nem sempre é ótimo. Componentes de UI são difíceis de testar isoladamente — para eles, testes snapshot ou testes de regressão visual (Percy, Chromatic) são mais eficazes. Prototipagem e investigação (spike solutions) não requerem testes. Código legado sem testes é difícil de cobrir via TDD — aqui, primeiro são necessários testes de caracterização (testes que capturam o comportamento atual antes da refatoração). Nestes casos, TDD não é completamente abandonado, mas adaptado — os testes são escritos para a funcionalidade alterada, não para todo o código legado.
O desenvolvimento móvel tem as suas especificidades: a lógica de negócio está frequentemente misturada com código de UI (Activity, ViewController, ViewModel), o que dificulta os testes unitários. A melhor prática é Views finas, ViewModels grossos: extrair toda a lógica dos componentes de UI para classes separadas (UseCase, Repository, ViewModel) que sejam fáceis de testar sem emulador. Android e iOS têm frameworks nativos de testes unitários que funcionam em JVM/Native sem iniciar um dispositivo.
Os testes unitários Android são executados numa JVM local sem emulador, proporcionando velocidade de execução — um teste típico demora menos de 100ms. JUnit 5 é o runner principal. Para testes de ViewModel, use kotlinx-coroutines-test para testar corrotinas e Turbine para testar StateFlow. Robolectric permite testar componentes dependentes do Android (Context, Resources) sem emulador, carregando shadow-classes. Para testes Compose, use Compose UI Test — mas estes são testes de UI, não unitários.
Os testes unitários iOS são escritos em Swift com XCTest (integrado no Xcode). Quick + Nimble são frameworks BDD para testes mais legíveis (describe/context/it). Para mocking, use Cuckoo (geração de mocks) ou SwiftyMocky. Swift suporta protocolos e injeção de dependências, facilitando a substituição de dependências. Ponto chave: os testes unitários iOS são executados no simulador macOS, não num dispositivo real. Testes que requerem funcionalidades de hardware (câmara, Bluetooth) são testes de integração.
Os testes unitários Flutter usam o pacote flutter_test e são executados na Dart VM sem emulador. Para mocking, use o pacote mockito com geração de código (build_runner). Testes de widgets (no mesmo pacote) testam widgets individuais mas requerem renderização e são mais lentos — use-os apenas para verificação de lógica de UI. Lógica Dart pura (modelos, repositórios, blocs) é testada como testes Dart regulares sem importar flutter_test.
// Exemplo de teste unitário em Flutter com mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
Testes unitários eficazes requerem disciplina. A regra principal: teste o comportamento, não a implementação. O teste não deve saber como o módulo está implementado internamente (quais métodos privados são chamados, em que ordem). Se um teste está ligado à implementação, quebra em cada refatoração e perde valor. O teste verifica o contrato: com entrada X, a saída deve ser Y. A exceção são testes para algoritmos com desempenho crítico, onde a sequência de chamadas é importante.
Cobertura de 100% é um objetivo inatingível e desnecessário. De acordo com Google Testing Blog (2025), o nível ideal de cobertura para testes unitários é 70-80% das linhas de código. A cobertura de 100% é muitas vezes alcançada testando getters, setters e construtores, o que não acrescenta valor. Concentre-se na lógica de negócio crítica: cálculos complexos, validação, tratamento de erros, casos limite. Use JaCoCo (Java), Coverage.py (Python), Istanbul (JS) para medição e defina um limite no CI — falha de compilação com cobertura abaixo de 60%.
Os testes unitários são a primeira etapa de qualquer pipeline CI/CD. São executados em cada push ao repositório, antes da compilação e implantação. O tempo médio de execução do conjunto de testes unitários não deve exceder 5 minutos — se for maior, os testes deixam de ser "rápidos" e os desenvolvedores param de os executar localmente. Separe os testes em rápidos (unitários) e lentos (integração) e execute-os em diferentes etapas do pipeline. Use execução paralela e fail-fast para acelerar.
Perguntas frequentes
Um teste unitário verifica um único módulo isoladamente, substituindo dependências externas por mocks. Um teste de integração verifica a interação entre vários componentes reais (BD, API, sistema de ficheiros). Os testes unitários são executados em milissegundos, os de integração em segundos. Na pirâmide de testes, os unitários ocupam 70%.
A escolha depende da plataforma: JUnit 5 para Java/Kotlin, XCTest para iOS/Swift, pytest para Python, Jest/Vitest para JavaScript/TypeScript, flutter_test para Flutter. Para mocking, use Mockito (Java), Cuckoo (iOS), unittest.mock (Python) ou vitest.mock (JS). Todos os frameworks modernos suportam testes parametrizados, assertions integradas e execução paralela.
Fast — o teste executa em milissegundos. Isolated — não depende de outros testes ou sistemas externos. Repeatable — dá o mesmo resultado em qualquer máquina. Self-validating — verifica automaticamente o resultado. Timely — escrito antes ou sincronamente com o código. Violar qualquer princípio reduz a eficácia dos testes.
Sim, absolutamente. O ViewModel contém lógica de negócio — tratamento de eventos, transformação de dados, gestão de estado. No Android, use kotlinx-coroutines-test para corrotinas e Turbine para testar StateFlow. No iOS, teste Combine Publishers ou async/await no ViewModel. Testes de ViewModel são testes unitários puros executados em JVM/macOS sem emulador.
Os pedidos de rede em testes unitários não são executados — são substituídos por mocks do cliente HTTP. No Android, use MockWebServer (OkHttp) — executa um servidor HTTP local, preferível a mocks porque reproduz a interação real de rede. MockWebServer proporciona isolamento sem perder realismo. Para iOS — OHHTTPStubs ou URLProtocol para intercetar e substituir respostas.
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