Testes em Swift: da teoria à prática com mocks e fixtures
Imagine que você está assando um bolo e acabou a farinha. Você não pode verificar se o creme vai dar certo até ir ao mercado. Mais ou menos assim se sente um desenvolvedor que escreve testes unitários para código que trabalha com rede, banco de dados ou dependências complexas. Em vez de esperar pelo servidor real ou pelo banco de dados real, usamos objetos mock e fixtures em testes Swift — são nossos "substitutos da farinha", que nos permitem testar pedaços isolados de código aqui e agora.
Neste artigo, não vamos apenas listar definições. Vamos analisar como aplicar corretamente os testes Swift na prática, quando usar mocks e quando usar stubs, e como as fixtures ajudam a não enlouquecer na preparação de dados. Vamos lá!
O que são mock, stub e fixture? Entendendo os termos
Antes de escrever código, vamos combinar os conceitos. No mundo dos testes unitários iOS, muitas vezes confundem-se mocks e stubs. Lembre-se da regra simples:
- Stub — um objeto que retorna dados pré-definidos. Ele não verifica como foi chamado. Apenas fornece a resposta.
- Mock — um objeto que não só retorna dados, mas também verifica se determinados métodos foram chamados e com quais parâmetros. É um "espião".
- Fixture — um conjunto de dados pré-preparado (geralmente JSON ou uma estrutura) usado nos testes para criar um ambiente previsível.
Em testes Swift, frequentemente combinamos essas abordagens. Por exemplo, carregamos uma fixture JSON, fazemos o parsing e depois substituímos a camada de rede por um mock que verifica se a requisição foi enviada para a URL correta.
Escrevendo o primeiro mock em Swift com protocolos
A maneira mais comum de criar objetos mock em Swift é usar protocolos. Em um projeto real, você tem um serviço que faz requisições de rede. Nos testes, você o substitui por um mock.
Aqui está um exemplo de abstração:
// Protocolo para a camada de redeprotocol NetworkServiceProtocol { func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void)}
// Implementação real (faz requisições de rede)class RealNetworkService: NetworkServiceProtocol { func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) { // URLSession, Alamofire, etc. }}
// Mock para testesclass MockNetworkService: NetworkServiceProtocol { var fetchUsersCalled = false var resultToReturn: Result<[User], Error> = .success([])
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) { fetchUsersCalled = true completion(resultToReturn) }}Agora, no teste, podemos verificar se o método foi chamado:
func testViewModelFetchesUsers() { let mockService = MockNetworkService() mockService.resultToReturn = .success([User(id: 1, name: "Test")])
let viewModel = UserViewModel(networkService: mockService) viewModel.loadUsers()
XCTAssertTrue(mockService.fetchUsersCalled, "O método fetchUsers deve ser chamado")}Observe: estamos usando injeção de dependência Swift — passamos a dependência pelo inicializador. Este é um padrão chave para testes.
Fixtures: como não escrever 100500 linhas nos testes
Quando você testa parsing de JSON ou trabalho com CoreData, precisa de dados reais. Mas gerá-los toda vez no código é um inferno. É aí que entram as fixtures em testes Swift.
Crie um arquivo users_fixture.json na pasta de testes:
[ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"}]Agora