Objetivo dos testes no iOS
Testes automatizados ajudam a detectar regressões cedo e dão segurança para refatorar. Em iOS, o Xcode oferece dois tipos principais: testes de unidade (validam funções, regras de negócio e ViewModels) e UI tests (validam fluxos críticos pela interface). A ideia é manter a maior parte da lógica coberta por testes de unidade (rápidos) e usar UI tests para poucos caminhos essenciais (mais lentos e frágeis).
Configurando testes no Xcode
Criando targets de teste
Ao criar um projeto, o Xcode normalmente já oferece as opções de incluir targets de teste. Se você precisar adicionar depois:
- No Xcode, vá em
File > New > Target... - Escolha
iOS Unit Testing Bundlepara testes de unidade - Escolha
iOS UI Testing Bundlepara UI tests - Confirme o app alvo (Target) que será testado
Onde os testes ficam e como são organizados
Você terá dois grupos/pastas no projeto: um para AppNameTests (unidade) e outro para AppNameUITests (UI). Dentro deles, crie arquivos por responsabilidade, por exemplo: ValidationTests, FormattingTests, ItemListViewModelTests, CriticalFlowUITests.
Importante: @testable import
Em testes de unidade, use @testable import SeuModulo para acessar tipos internos do módulo do app. Isso facilita testar ViewModels e regras sem expor tudo como public.
import XCTest @testable import MeuAppXCTest: estrutura básica
Um teste de unidade é um método que começa com test dentro de uma classe que herda de XCTestCase. Use setUp() para preparar dependências e tearDown() para limpar.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
import XCTest @testable import MeuApp final class ValidationTests: XCTestCase { override func setUp() { super.setUp() // preparar objetos comuns aos testes } override func tearDown() { // limpar recursos super.tearDown() } func testExample() { XCTAssertTrue(true) } }Asserções mais usadas
| Asserção | Uso |
|---|---|
XCTAssertEqual(a, b) | Comparar valores |
XCTAssertTrue(cond) | Condição deve ser verdadeira |
XCTAssertFalse(cond) | Condição deve ser falsa |
XCTAssertNil(x) | Deve ser nil |
XCTAssertNotNil(x) | Não deve ser nil |
XCTFail("mensagem") | Falha explícita |
Testes de unidade para funções: validação, formatação e parsing
1) Validação (ex.: e-mail e senha)
Uma boa prática é colocar validações em tipos simples (struct/enum) para facilitar o teste. Exemplo de validador:
enum Validator { static func isValidEmail(_ value: String) -> Bool { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.contains("@"), trimmed.contains(".") else { return false } return trimmed.count >= 5 } static func isValidPassword(_ value: String) -> Bool { value.count >= 8 } }Testes:
final class ValidatorTests: XCTestCase { func testEmailValidation_acceptsBasicValidEmail() { XCTAssertTrue(Validator.isValidEmail("ana@exemplo.com")) } func testEmailValidation_rejectsMissingAt() { XCTAssertFalse(Validator.isValidEmail("anaexemplo.com")) } func testPasswordValidation_requiresMinimumLength() { XCTAssertFalse(Validator.isValidPassword("1234567")) XCTAssertTrue(Validator.isValidPassword("12345678")) } }2) Formatação (ex.: moeda e datas)
Formatação costuma depender de locale/timezone. Para testes determinísticos, injete dependências (por exemplo, Locale e TimeZone) ou fixe valores no formatter.
struct CurrencyFormatter { let locale: Locale func formatBRL(_ value: Decimal) -> String { let formatter = NumberFormatter() formatter.numberStyle = .currency formatter.currencyCode = "BRL" formatter.locale = locale return formatter.string(from: value as NSDecimalNumber) ?? "" } }final class CurrencyFormatterTests: XCTestCase { func testFormatBRL_ptBR() { let sut = CurrencyFormatter(locale: Locale(identifier: "pt_BR")) let result = sut.formatBRL(Decimal(string: "10.5")!) XCTAssertEqual(result, "R$ 10,50") } }Se seu ambiente variar e o separador/espaço mudar, compare padrões (ex.: contém R$ e 10) ou normalize espaços. O ideal é padronizar o formatter para o teste.
3) Parsing/decodificação de dados (JSON)
Para testar parsing, use um JSON pequeno e determinístico. Exemplo de modelo:
struct ItemDTO: Decodable, Equatable { let id: Int let title: String let isDone: Bool }Teste de decodificação:
final class ParsingTests: XCTestCase { func testDecodeItemDTO() throws { let json = """ { "id": 1, "title": "Comprar leite", "isDone": false } """.data(using: .utf8)! let decoded = try JSONDecoder().decode(ItemDTO.self, from: json) XCTAssertEqual(decoded, ItemDTO(id: 1, title: "Comprar leite", isDone: false)) } func testDecodeFailsWithMissingField() { let json = """ { "id": 1, "isDone": false } """.data(using: .utf8)! XCTAssertThrowsError(try JSONDecoder().decode(ItemDTO.self, from: json)) } }Testes de unidade para ViewModels (com async/await)
ViewModels geralmente coordenam validação, chamadas de serviço e atualização de estado. Para testá-los bem, evite dependências reais (rede) e injete um protocolo para o serviço.
1) Definindo um protocolo para o serviço
protocol ItemsService { func fetchItems() async throws -> [ItemDTO] func saveItem(title: String) async throws -> ItemDTO }2) Criando um mock simples
Um mock pode retornar sucesso ou erro, e também registrar chamadas para você validar comportamento.
final class ItemsServiceMock: ItemsService { enum MockError: Error { case forced } var fetchItemsResult: Result<[ItemDTO], Error> = .success([]) var saveItemResult: Result<ItemDTO, Error> = .success(ItemDTO(id: 1, title: "X", isDone: false)) private(set) var fetchItemsCallCount = 0 private(set) var saveItemCallCount = 0 private(set) var lastSavedTitle: String? func fetchItems() async throws -> [ItemDTO] { fetchItemsCallCount += 1 return try fetchItemsResult.get() } func saveItem(title: String) async throws -> ItemDTO { saveItemCallCount += 1 lastSavedTitle = title return try saveItemResult.get() } }3) Um ViewModel exemplo (estado + validação)
@MainActor final class ItemsViewModel: ObservableObject { @Published private(set) var items: [ItemDTO] = [] @Published var titleInput: String = "" @Published private(set) var errorMessage: String? private let service: ItemsService init(service: ItemsService) { self.service = service } func load() async { do { items = try await service.fetchItems() errorMessage = nil } catch { errorMessage = "Falha ao carregar" } } func canSave() -> Bool { let trimmed = titleInput.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.count >= 3 } func save() async { guard canSave() else { errorMessage = "Título inválido" return } do { let created = try await service.saveItem(title: titleInput) items.insert(created, at: 0) errorMessage = nil } catch { errorMessage = "Falha ao salvar" } } }4) Testando o ViewModel (sucesso, erro e validação)
Como os métodos são async, seus testes também podem ser async.
final class ItemsViewModelTests: XCTestCase { func testLoad_success_updatesItemsAndClearsError() async { let mock = ItemsServiceMock() mock.fetchItemsResult = .success([ItemDTO(id: 1, title: "A", isDone: false)]) let sut = await ItemsViewModel(service: mock) await sut.load() let items = await sut.items let error = await sut.errorMessage XCTAssertEqual(mock.fetchItemsCallCount, 1) XCTAssertEqual(items.count, 1) XCTAssertNil(error) } func testLoad_failure_setsErrorMessage() async { let mock = ItemsServiceMock() mock.fetchItemsResult = .failure(ItemsServiceMock.MockError.forced) let sut = await ItemsViewModel(service: mock) await sut.load() let error = await sut.errorMessage XCTAssertEqual(mock.fetchItemsCallCount, 1) XCTAssertEqual(error, "Falha ao carregar") } func testSave_invalidTitle_doesNotCallService() async { let mock = ItemsServiceMock() let sut = await ItemsViewModel(service: mock) await MainActor.run { sut.titleInput = " " } await sut.save() XCTAssertEqual(mock.saveItemCallCount, 0) let error = await sut.errorMessage XCTAssertEqual(error, "Título inválido") } func testSave_validTitle_callsServiceAndInsertsItem() async { let mock = ItemsServiceMock() mock.saveItemResult = .success(ItemDTO(id: 99, title: "Novo", isDone: false)) let sut = await ItemsViewModel(service: mock) await MainActor.run { sut.titleInput = "Novo" } await sut.save() XCTAssertEqual(mock.saveItemCallCount, 1) XCTAssertEqual(mock.lastSavedTitle, "Novo") let items = await sut.items XCTAssertEqual(items.first?.id, 99) } }Observação: em alguns casos, acessar propriedades @Published pode exigir estar no MainActor. Se o compilador reclamar, use await MainActor.run { ... } para ler/escrever estado do ViewModel.
Introdução a UI Tests: validando fluxos críticos
UI tests usam XCUITest para abrir o app, encontrar elementos e interagir como um usuário. Eles são ideais para validar caminhos como: abrir app, navegar, preencher formulário e salvar um item.
Pré-requisito: accessibilityIdentifier
Para que o UI test encontre elementos de forma estável, defina accessibilityIdentifier nos componentes importantes. Exemplo (em uma tela com lista e botão de adicionar):
// Exemplo em SwiftUI Button("Adicionar") { /* ... */ } .accessibilityIdentifier("items.addButton") TextField("Título", text: $viewModel.titleInput) .accessibilityIdentifier("items.titleField") Button("Salvar") { /* ... */ } .accessibilityIdentifier("items.saveButton") List(items) { item in Text(item.title) } .accessibilityIdentifier("items.list")Dica: use um padrão de nomes consistente, como tela.elemento (ex.: items.saveButton).
Estrutura básica de um UI test
import XCTest final class CriticalFlowUITests: XCTestCase { func testCreateItemFlow() { let app = XCUIApplication() app.launch() // interações e asserts aqui } }Passo a passo: abrir app, navegar, preencher e salvar
Exemplo de fluxo: tocar em “Adicionar”, digitar título, salvar e verificar que o item aparece na lista.
final class CriticalFlowUITests: XCTestCase { func testCreateItemFlow() { let app = XCUIApplication() app.launch() let addButton = app.buttons["items.addButton"] XCTAssertTrue(addButton.waitForExistence(timeout: 2)) addButton.tap() let titleField = app.textFields["items.titleField"] XCTAssertTrue(titleField.waitForExistence(timeout: 2)) titleField.tap() titleField.typeText("Comprar pão") let saveButton = app.buttons["items.saveButton"] XCTAssertTrue(saveButton.waitForExistence(timeout: 2)) saveButton.tap() let list = app.otherElements["items.list"] XCTAssertTrue(list.waitForExistence(timeout: 2)) XCTAssertTrue(app.staticTexts["Comprar pão"].waitForExistence(timeout: 2)) } }Notas práticas:
waitForExistence(timeout:)reduz flakiness (testes que falham por timing).- Se o elemento estiver dentro de uma célula de lista, às vezes é melhor buscar por
staticTextscom o texto esperado (como no exemplo). - Se houver navegação entre telas, identifique também o título da tela ou um elemento âncora com
accessibilityIdentifier.
Controlando dados no UI test (modo de teste)
UI tests ficam mais confiáveis quando o app inicia em um estado conhecido. Uma abordagem comum é passar argumentos de launch e, no app, detectar isso para usar um repositório em memória ou limpar persistência.
No UI test:
let app = XCUIApplication() app.launchArguments = ["--ui-testing", "--reset-data"] app.launch()No app (em um ponto central de inicialização), você pode ler:
let isUITesting = ProcessInfo.processInfo.arguments.contains("--ui-testing")Com isso, você consegue evitar que dados antigos quebrem o teste (por exemplo, itens já existentes na lista).
Rodando testes no simulador e interpretando falhas
Como rodar testes
- Rodar todos os testes:
Product > Test - Rodar um arquivo/classe: clique no losango ao lado do nome da classe de teste
- Rodar um teste específico: clique no losango ao lado do método
test... - Escolha um simulador no topo do Xcode (UI tests rodam no simulador/dispositivo)
Entendendo falhas (unit tests)
Quando um XCTAssert... falha, o Xcode mostra:
- Qual asserção falhou e os valores comparados
- Arquivo e linha exata
- Stack trace no painel de debug
Boas práticas para facilitar diagnóstico:
- Use mensagens em asserts quando fizer sentido:
XCTAssertEqual(a, b, "Mensagem") - Teste uma coisa por vez (arrange/act/assert) para a falha ser óbvia
- Evite dependências externas (rede, relógio, locale variável) sem controle
Entendendo falhas (UI tests)
Falhas comuns em UI tests:
- Elemento não encontrado: geralmente falta
accessibilityIdentifier, o elemento está em outra tela, ou o teste não esperou o carregamento. UsewaitForExistencee valide a navegação. - Teclado/Focus: o campo não recebeu foco. Garanta
tap()antes detypeText. - Estado inicial inconsistente: dados antigos. Use launch arguments para resetar dados.
O Xcode também pode gerar artefatos de UI test (como capturas em falhas) dependendo da configuração. Ao investigar, observe a árvore de elementos no debug de UI test e confirme se o identificador está correto.
Relatórios e navegação no Test Navigator
No Test Navigator (ícone de losango), você vê:
- Lista de suites, classes e métodos
- Status (pass/fail) e tempo de execução
- Histórico recente de execuções
Ao clicar em uma falha, o Xcode leva direto à linha do teste e mostra detalhes do erro. Em UI tests, se a falha for por inexistência de elemento, revise primeiro o identificador e a tela atual no momento da asserção.