Testes Avançados em Swift: Unit, Integration e E2E
Testes não são apenas uma verificação, mas a base de um código confiável. Em um nível avançado, o desenvolvedor Swift não precisa apenas escrever testes, mas construir adequadamente a pirâmide de testes: desde testes unitários rápidos até cenários complexos de ponta a ponta (e2e). Neste artigo, abordaremos técnicas avançadas para projetos iOS/macOS: trabalho com dependências, testes de integração CoreData e automação de UI.
1. Testes Unitários: Gerenciamento de Dependências e Mocks
Em um nível avançado, os testes unitários devem ser isolados. A técnica chave é a Injeção de Dependência (DI). Em vez de testar a camada de rede real ou o banco de dados, substituímos por mocks.
Exemplo: Protocolo e DI
protocol NetworkServiceProtocol { func fetchData(completion: @escaping (Result<String, Error>) -> Void)}
class ViewModel { private let networkService: NetworkServiceProtocol init(networkService: NetworkServiceProtocol) { self.networkService = networkService } func loadData() { networkService.fetchData { result in // processamento } }}
// Mock para testesclass MockNetworkService: NetworkServiceProtocol { var shouldReturnError = false func fetchData(completion: @escaping (Result<String, Error>) -> Void) { if shouldReturnError { completion(.failure(NSError(domain: "test", code: -1))) } else { completion(.success("Mock data")) } }}No teste, você cria um MockNetworkService e o passa para o ViewModel. Isso permite verificar cenários de erro e sucesso sem uma API real.
XCTestExpectation para Assincronia
func testViewModelLoadDataSuccess() { let mock = MockNetworkService() let viewModel = ViewModel(networkService: mock) let expectation = self.expectation(description: "Data loaded") viewModel.loadData() // Simula chamada assíncrona DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } waitForExpectations(timeout: 1.0) // Verifica o estado do ViewModel}2. Testes de Integração: CoreData e Trabalho com Dados Reais
Os testes de integração verificam a interação entre componentes. Para CoreData, usamos um contêiner in-memory para não poluir o banco de dados principal.
Configuração do CoreData In-Memory
class CoreDataTestStack { lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "MyModel") let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType container.persistentStoreDescriptions = [description] container.loadPersistentStores { _, error in if let error = error { fatalError("Failed to load store: \\(error)") } } return container }() var context: NSManagedObjectContext { return persistentContainer.viewContext }}Exemplo de Teste de Integração
func testSaveAndFetchUser() { let stack = CoreDataTestStack() let repository = UserRepository(context: stack.context) // Cria um usuário let user = User(context: stack.context) user.name = "Test User" user.age = 30 try? stack.context.save() // Verifica se os dados foram salvos let fetchRequest: NSFetchRequest<User> = User.fetchRequest() let users = try? stack.context.fetch(fetchRequest) XCTAssertEqual(users?.count, 1) XCTAssertEqual(users?.first?.name, "Test User")}Importante: após cada teste, limpe o contexto para que os testes não interfiram uns nos outros.