Testes em Swift: Unit, Integration, E2E — da teoria à prática
Testes de qualidade não são apenas uma "boa prática" no desenvolvimento para iOS e macOS, mas uma etapa criticamente importante que garante estabilidade, segurança e previsibilidade do aplicativo. Neste artigo, vamos explorar três níveis de testes em Swift: Testes Unitários, Testes de Integração e E2E (end-to-end). Você aprenderá como eles diferem, quando aplicá-los e como escrever testes eficazes usando os frameworks XCTest e XCUITest.
1. Testes Unitários: verificando os "blocos de construção" do aplicativo
Testes unitários são o tipo de teste mais básico e mais rápido. Eles verificam o funcionamento de funções, métodos ou classes individuais (unidades) isoladamente de dependências externas (rede, banco de dados, UI).
Princípios básicos dos Testes Unitários em Swift:
- Isolamento: o código testado não deve depender do estado de outros módulos.
- Reprodutibilidade: o teste deve dar o mesmo resultado a cada execução.
- Rapidez: milhares de testes unitários devem ser executados em segundos.
Exemplo de Teste Unitário com XCTest:
import XCTest
class Calculator { func add(_ a: Int, _ b: Int) -> Int { return a + b } func divide(_ a: Int, _ b: Int) throws -> Int { guard b != 0 else { throw CalculationError.divisionByZero } return a / b }}
enum CalculationError: Error { case divisionByZero}
class CalculatorTests: XCTestCase { var calculator: Calculator! override func setUp() { super.setUp() calculator = Calculator() } override func tearDown() { calculator = nil super.tearDown() } func testAddition() { // Given let a = 5 let b = 3 // When let result = calculator.add(a, b) // Then XCTAssertEqual(result, 8, "5 + 3 deve ser igual a 8") } func testDivisionByZeroThrowsError() { // Usamos XCTAssertThrowsError para verificar exceções XCTAssertThrowsError(try calculator.divide(10, 0)) { error in XCTAssertEqual(error as? CalculationError, .divisionByZero) } }}Melhores práticas para Testes Unitários:
- Use o padrão AAA (Arrange-Act-Assert) — preparação, ação, verificação.
- Nomeie os testes de forma significativa:
testMethodName_condition_expectedResult. - Evite testar métodos private — teste a interface pública.
- Para mocks e stubs, use protocolos e dependency injection.
2. Testes de Integração: verificando a interação entre componentes
Testes de integração verificam como diferentes módulos do aplicativo funcionam juntos. No contexto do iOS, isso pode ser a interação da ViewModel com a rede, Core Data ou UserDefaults.
Exemplo de Teste de Integração com mock da camada de rede:
import XCTest
// Protocolo para o serviço de rede (permite substituição fácil)protocol NetworkServiceProtocol { func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)}
// Serviço real (não o testamos no teste de integração)class RealNetworkService: NetworkServiceProtocol { func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) { // Requisição HTTP real }}
// Mock do serviço para testeclass MockNetworkService: NetworkServiceProtocol { var shouldReturnError = false var mockUser = User(id: 1, name: "Test User") func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) { if shouldReturnError { completion(.failure(NSError(domain: "test", code: 500)))