Swift Testing: O framework que substitui o XCTest, e o que permanece no XCTest
O XCTest, framework de testes da Apple desde 2013, está sendo substituído pelo Swift Testing. A Apple ainda não diz “obsoleto” (o XCTest ainda é fornecido, ainda funciona, ainda hospeda automação de UI e testes de desempenho), mas toda sessão da Apple, toda thread no Swift Forum e todo novo projeto de exemplo desde a WWDC 2024 direcionam o código Swift ao Swift Testing. O framework é fornecido com o Xcode 16+ e o Swift 61, roda em macOS, iOS, watchOS, tvOS, visionOS e Linux2, e apresenta um modelo mental diferente: testes são funções, suítes são tipos, configuração é traits, e paralelismo é o padrão.
A migração é incremental. A própria documentação da Apple suporta explicitamente a execução dos dois frameworks lado a lado no mesmo target3, o que significa que a decisão certa para uma base de código não trivial não é “reescrever tudo”, mas “escrever novos testes em Swift Testing e migrar os antigos conforme você os toca”. O post percorre a API do Swift Testing, identifica a fronteira de migração (o que permanece no XCTest), e cobre o vocabulário de traits que substitui a configuração baseada em hierarquia de classes do XCTest.
TL;DR
- O Swift Testing substitui o modelo baseado em classes e tipado por strings do XCTest por macros:
@Test,@Suite,#expect(...)e#require(...)4. - Testes são funções livres ou métodos em tipos; suítes são qualquer tipo que contenha funções de teste (
@Suitesó é necessário ao especificar um nome de exibição ou trait). - A configuração migra de hierarquias de classes e convenções de nomenclatura para traits:
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...)5. - Testes parametrizados via
@Test(arguments:)produzem um resultado de teste pai com um filho por argumento; paralelos por padrão6. - A migração é lado a lado, não big-bang. Automação de UI (
XCUIApplication) e testes de desempenho (XCTMetric) permanecem no XCTest porque o Swift Testing não os cobre3. - A WWDC 2025 adicionou anexos personalizados (artefatos de teste) e exit tests (verificação de código que se espera terminar sob condições específicas)7.
Por que o Swift Testing existe
Os pontos problemáticos do XCTest são bem conhecidos por qualquer um que tenha mantido uma grande suíte de testes iOS:
Asserções tipadas por strings. XCTAssertEqual(a, b, "message") não captura a expressão Swift original. Quando a asserção falha, a mensagem de falha mostra os valores em tempo de execução, mas não qual expressão os produziu. Depurar exige ler o código-fonte do teste e reconstruir o contexto.
Configuração baseada em classes. Setup, teardown e estado compartilhado exigem subclasses de XCTestCase. Desabilitar um teste exige renomeá-lo (para que o padrão de descoberta o ignore) ou envolvê-lo em #if DEBUG. Selecionar quais testes rodam exige nomenclatura cuidadosa.
Incompatibilidade com concorrência. O XCTest é anterior à Swift Concurrency. XCTestExpectation mais wait(for:timeout:) é a ponte legada para código assíncrono; o padrão é verboso e propenso a erros. Código Swift moderno usa async/await; os idioms do XCTest parecem anacrônicos.
Parametrização fraca. O XCTest não tem suporte de primeira classe para testes parametrizados. Desenvolvedores fingem com loops for dentro de métodos de teste (que produz um resultado de teste para muitos casos) ou escrevem geradores de código (que produz N métodos de teste a partir de um template).
O Swift Testing aborda cada um:
import Testing
@Test func userInitializesWithDefaults() {
let user = User()
#expect(user.name == "Anonymous")
#expect(user.preferences.isEmpty)
}
#expect captura a expressão completa. Quando a asserção falha, a saída do teste mostra user.name == "Anonymous" falhou porque user.name era "Guest". O framework faz o trabalho de ler a expressão porque a macro vê a árvore sintática. Sem suposições “esperado X, obtido Y”.
As macros
Quatro macros fazem o trabalho.
@Test e @Suite
@Test marca uma função como um teste. A função pode estar no escopo do arquivo, em um struct, em um actor, em um enum, em qualquer lugar onde uma função possa estar em Swift. O framework descobre as funções marcadas com @Test e as executa.
@Test func loginSucceedsWithValidCredentials() async throws {
let result = try await api.login(username: "alice", password: "valid")
#expect(result.isAuthenticated)
}
struct AuthenticationTests {
@Test func loginRejectsEmptyPassword() async throws {
let result = try await api.login(username: "alice", password: "")
#expect(!result.isAuthenticated)
#expect(result.error == .emptyPassword)
}
}
@Suite marca um tipo como uma suíte de testes explicitamente. O marcador é implícito quando um tipo contém funções @Test; @Suite explícito só é necessário ao adicionar um nome de exibição personalizado ou aplicar uma trait no nível da suíte4.
@Suite("Authentication", .tags(.security))
struct AuthenticationTests {
@Test func loginSucceeds() async throws { ... }
@Test func loginFails() async throws { ... }
}
#expect e #require
#expect é a asserção padrão. O teste continua se a expectativa falhar (a falha é registrada; asserções subsequentes ainda rodam). #require é a variante fail-fast: se o requisito falhar, o teste para imediatamente. Use #require para precondições cuja falha produziria erros sem sentido em asserções subsequentes.
@Test func userPreferencesLoad() async throws {
let user = try #require(await store.fetchUser(id: 42))
// Se fetchUser retorna nil, o teste para aqui. A próxima linha
// nunca roda contra um optional desempacotado.
#expect(user.preferences["theme"] == "dark")
#expect(user.preferences.count >= 1)
}
O try antes de #require importa: #require retorna o valor desempacotado quando o requisito é satisfeito (neste caso, o User não opcional) ou lança quando não é. A função de teste deve ser throws para usar #require para desempacotamento.
Testes parametrizados
@Test(arguments:) roda a função de teste uma vez por argumento. O framework reporta cada argumento como um teste filho com o teste pai como o agrupamento.
@Test(arguments: [
("alice", "password123", true),
("bob", "", false),
("", "password123", false),
("carol", "wrongpassword", false),
])
func loginScenario(username: String, password: String, expectedSuccess: Bool) async throws {
let result = try await api.login(username: username, password: password)
#expect(result.isAuthenticated == expectedSuccess)
}
O framework roda os quatro argumentos em paralelo por padrão. Mensagens de falha de teste identificam qual tupla de argumento falhou, então depurar uma falha parametrizada não exige rodar o teste isoladamente.
Para argumentos que são coleções independentes, @Test(arguments: arrayA, arrayB) roda o produto cruzado (toda combinação). Para pares sequenciais, use zip(arrayA, arrayB) e passe a sequência resultante.
Traits: configuração como valores de primeira classe
O XCTest usa hierarquias de classes, convenções de nomenclatura e planos de teste do Xcode para configurar testes. O Swift Testing usa traits: valores aplicados às declarações @Test e @Suite5:
@Test(.enabled(if: ProcessInfo.processInfo.isRunningOnCI))
func ciOnlyIntegrationTest() async throws { ... }
@Test(.disabled("Pending fix for FB12345"))
func knownFailingTest() { ... }
@Test(.timeLimit(.minutes(1)))
func potentiallySlowTest() async throws { ... }
@Test(.tags(.network, .integration), .bug("FB67890", "regression in retry logic"))
func networkRetryTest() async throws { ... }
@Suite(.serialized)
struct DatabaseTests {
// Todos os testes nesta suíte rodam sequencialmente.
// Útil quando testes compartilham uma fixture de banco de dados.
@Test func insertUser() { ... }
@Test func updateUser() { ... }
@Test func deleteUser() { ... }
}
O vocabulário de traits cobre os casos para os quais o XCTest exigia infraestrutura customizada:
.enabled(if:)e.disabled(_:). Habilitação condicional com uma expressão em tempo de execução e um motivo opcional. Substitui blocos#if DEBUGe truques de renomeação..serialized. Marca uma suíte ou teste parametrizado como sequencial. Herdável: uma suíte serializada força todos os testes filhos a serem seriais6..timeLimit(...). Limite de tempo por teste. O teste é encerrado e reportado como falho se exceder o limite..tags(...). Tags para filtragem. Configure regras de inclusão/exclusão de tags em um plano de teste do Xcode; o plano roda viaxcodebuild test -testPlan <name>. Note que-only-testing:filtra por identificador de teste (target, suíte ou função), não por tag..bug(...). Vincula um teste a um ID de bug com um motivo. O link aparece no relatório de teste e (para testes que se sabe estarem falhando) impede que a falha bloqueie o CI.
Traits personalizadas também são suportadas. Um app pode definir suas próprias traits para necessidades específicas do projeto (por exemplo, .requiresKeychain, .skipIfOffline).
Paralelismo é o padrão
O Swift Testing roda testes em paralelo por padrão, incluindo argumentos parametrizados. O padrão importa porque a maioria das suítes de teste cresce ao longo do tempo, e paralelismo é a diferença entre uma execução de teste de 30 segundos e uma de 5 minutos. O XCTest exigia opt-in explícito (alternância de paralelismo de teste no plano de teste); o Swift Testing inverte o padrão.
A implicação: testes precisam ser independentes. Estado compartilhado (uma fixture de banco de dados, um sistema de arquivos mockado, um singleton no nível do app) precisa ser isolado por teste (cada teste recebe uma fixture nova) ou marcado como .serialized (a suíte roda sequencialmente). Testes que mutam estado global e assumem execução serial são bugs que o Swift Testing expõe mais rápido do que o XCTest expunha.
Migração: lado a lado, não big-bang
A posição oficial da Apple sobre migração é incremental3:
- Ambos os frameworks coexistem no mesmo target de teste.
- Novos testes podem ser escritos em Swift Testing imediatamente.
- Testes XCTest antigos continuam funcionando sem alterações.
- Migre testes XCTest para Swift Testing quando for conveniente (um arquivo tocado, um novo recurso, um refactor).
Dois casos permanecem no XCTest indefinidamente:
Testes de automação de UI. XCUIApplication, XCUIElement, XCUIElementQuery, toda a API de testes de UI, são exclusivos do XCTest3. O Swift Testing não fornece um substituto. Testes de UI permanecem no XCTest até que a Apple estenda a cobertura do Swift Testing.
Testes de desempenho. XCTMetric, measure(metrics:), baselines de desempenho, também exclusivos do XCTest3. O Swift Testing ainda não tem um equivalente.
Para todo o resto (testes unitários, testes de integração, testes assíncronos, testes parametrizados), o Swift Testing é o padrão recomendado para código novo.
O que a WWDC 2025 adicionou
Duas adições notáveis ao Swift Testing na WWDC 20257:
Anexos personalizados. Um teste pode anexar dados arbitrários Attachable ao seu resultado para triagem de falhas. Imagens de captura de tela falhas, logs de diagnóstico, arquivos de input gerados. Os anexos aparecem no test reporter do Xcode e em artefatos de CI. A API Attachment.record(_:named:sourceLocation:) em Sources/Testing/Attachments/Attachment.swift define a superfície; valores que conformam com Attachable (Data, String, tipos de imagem via conformidade opt-in) são aceitos.
@Test func reportLayoutAtFailure() async throws {
let result = await renderer.run(LayoutTestCase.complex)
if !result.passes {
Attachment.record(result.diagnostics, named: "diagnostics.txt")
Attachment.record(result.screenshotData, named: "failure-screenshot.png")
}
#expect(result.passes)
}
Exit tests. Testes que verificam código que se espera terminar (chamar exit(), lançar um erro fatal, disparar uma falha de precondição). O XCTest não tinha uma forma limpa de testar isso; os exit tests do Swift Testing rodam o código em um processo filho e verificam o comportamento de término.
@Test func parserExitsOnMalformedInput() async {
await #expect(processExitsWith: .failure) {
Parser.parse(malformedInput)
}
}
Ambas as adições são pragmáticas. Anexos personalizados resolvem um ponto problemático real (depurar falhas instáveis de CI); exit tests cobrem uma lacuna genuína na superfície de testes unitários.
Quando o XCTest ainda é a escolha certa
Três casos onde código novo ainda deve ser XCTest:
O teste é um teste de automação de UI. XCUIApplication.launch, interações de tap-and-swipe, queries baseadas em identificadores de acessibilidade. O Swift Testing não cobre a superfície de automação de UI; novos testes de UI são XCTest.
O teste é um benchmark de desempenho. measure(metrics: [XCTClockMetric()]), baselines de desempenho, medições de memória. O Swift Testing não tem equivalentes; novos testes de desempenho são XCTest.
O teste precisa rodar em uma plataforma que não suporta Swift Testing. O Swift Testing requer Swift 6 / Xcode 16+. Testes que precisam compilar contra toolchains anteriores (uma matriz de CI que inclui versões mais antigas do Xcode) precisam de XCTest até que a toolchain alcance.
Para testes unitários, testes de integração, testes assíncronos e testes parametrizados em targets Swift 6+, o Swift Testing é o padrão moderno. O custo de migração é real, mas o custo de não migrar acumula; cada novo XCTest escrito hoje é dívida técnica.
A conexão com workflow de agentes
Testes são o contrato contra o qual a geração de código assistida por IA trabalha. Quando um agente escreve código Swift, os testes são como o desenvolvedor (ou o próprio agente) verifica que a alteração está correta. As propriedades do Swift Testing tornam o código gerado por agentes mais verificável:
- Captura de expressão. A falha de
#expect(user.score == calculator.compute(user))mostra ambos os lados da comparação. Um agente lendo a falha pode corrigir o bug sem reler o arquivo de teste. - Casos parametrizados. Um agente corrigindo uma regressão sabe qual input específico falhou. A correção pode ser direcionada.
- Filtragem baseada em traits. Uma anotação
.tags(.regression)permite que um agente rode apenas testes de regressão após uma correção; loop de feedback mais rápido. - Anexos personalizados. Um agente depurando um teste instável pode anexar o artefato de input falho e lê-lo na próxima execução.
O post sobre hooks para desenvolvimento Apple cobre Stop hooks que rodam testes após cada edição de Claude Code. Falhas mais rápidas, mais paralelas e mais informativas do Swift Testing tornam esse loop apertado o suficiente para que o agente possa iterar sem intervenção manual.
O que esse padrão significa para apps iOS 26+
Três conclusões.
-
Padronize novos testes para Swift Testing. O framework é mais expressivo, roda em paralelo por padrão, captura contexto de falha automaticamente e suporta testes parametrizados como conceito de primeira classe. Código XCTest existente continua funcionando; código novo usa o padrão moderno.
-
Mantenha automação de UI e testes de desempenho no XCTest. Lacuna de cobertura da Apple, não escolha de migração. Até que o Swift Testing se estenda a esses domínios, esses testes permanecem no XCTest indefinidamente.
-
Use traits como vocabulário de configuração.
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...)cobrem os casos para os quais o XCTest exigia hierarquias de classes, convenções de nomenclatura e blocos#if DEBUG. Traits são valores de primeira classe que se compõem; o vocabulário de traits é o que faz o framework parecer moderno.
O cluster Apple Ecosystem completo: App Intents tipados; servidores MCP; a questão de roteamento; Foundation Models; a distinção LLM runtime vs ferramental; três superfícies; o padrão de fonte única da verdade; Dois Servidores MCP; hooks para desenvolvimento Apple; Live Activities; o contrato de runtime do watchOS; internals do SwiftUI; o modelo mental espacial do RealityKit; disciplina de schema do SwiftData; padrões de Liquid Glass; shipping multiplataforma; a matriz de plataformas; framework Vision; Symbol Effects; inferência Core ML; API Writing Tools; sobre o que me recuso a escrever. O hub está na Série Apple Ecosystem. Para contexto mais amplo de iOS com agentes de IA, veja o guia de Desenvolvimento iOS com Agentes.
FAQ
O Swift Testing substitui o XCTest inteiramente?
Ainda não. O XCTest ainda hospeda testes de automação de UI (XCUIApplication) e testes de desempenho (XCTMetric); o Swift Testing não cobre esses domínios. Para testes unitários, testes de integração, testes assíncronos e testes parametrizados, o Swift Testing é o padrão recomendado para código novo. Ambos os frameworks coexistem no mesmo target sem conflito.
Qual é a diferença prática entre #expect e XCTAssertEqual?
#expect captura a expressão Swift completa e reporta ambos os lados da comparação na falha (por exemplo, user.score == calculator.compute(user) falhou porque user.score = 80 e calculator.compute(user) = 100). XCTAssertEqual conhece apenas os valores em tempo de execução e a string de mensagem opcional. A captura de expressão é o que faz as falhas do Swift Testing serem autoexplicativas sem reler o código-fonte do teste.
Como funcionam os testes parametrizados?
@Test(arguments: [...]) roda a função de teste uma vez por argumento. O framework reporta cada argumento como um resultado de teste filho com o pai como o agrupamento. Tuplas podem passar múltiplos valores. Testes parametrizados rodam em paralelo por padrão; adicione .serialized para forçar execução sequencial.
Para que serve #require?
#require é a variante fail-fast de #expect. Use-o para precondições cuja falha produziria erros sem sentido em asserções subsequentes. try #require(value) retorna o valor desempacotado quando satisfeito ou lança quando não, então o resto do teste pode usar o valor desempacotado diretamente. A função de teste precisa ser throws para usar #require para desempacotamento.
Posso escrever testes Swift Testing no Linux?
Sim. O Swift Testing é open-source2 e roda em todas as plataformas que o Swift suporta, incluindo Linux. Projetos Swift server-side podem adotar o Swift Testing imediatamente. O framework não é específico de plataformas Apple.
Como migro uma suíte XCTest existente?
A recomendação da Apple é incremental: escreva novos testes em Swift Testing, migre os antigos quando você os tocar3. Não há um conversor automatizado (as diferenças do modelo são grandes o suficiente para que a automação seja frágil). O alvo certo de migração são testes unitários e testes de integração; testes de UI e testes de desempenho permanecem no XCTest indefinidamente.
Referências
-
Apple Developer: Swift Testing. Visão geral da Apple sobre a adoção do Swift Testing com Xcode 16+ e Swift 6. ↩
-
Repositório open-source: swiftlang/swift-testing no GitHub. Código-fonte do framework, disponível sob a Apache License 2.0 com a licença padrão do Swift Project. ↩↩
-
Documentação do Apple Developer: Migrating a test from XCTest. Guia oficial de migração da Apple cobrindo coexistência lado a lado e as exceções XCUIApplication / XCTMetric. ↩↩↩↩↩↩
-
Documentação do Apple Developer: Swift Testing. Referência do framework para
@Test,@Suite,#expect,#requiree o vocabulário de traits. ↩↩ -
Documentação do Apple Developer: Trait. O protocolo que define traits de teste, com traits embutidas incluindo
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...). ↩↩ -
Apple Developer: Meet Swift Testing (sessão WWDC 2024 10179) e Go further with Swift Testing (sessão WWDC 2024 10195). Sessões introdutórias cobrindo testes parametrizados e paralelismo. ↩↩
-
Evidências do repositório open-source para adições da WWDC 2025:
Sources/Testing/Attachments/Attachment.swiftpara anexos personalizados eSources/Testing/ExitTests/para a macro de exit test#expect(processExitsWith:). Ambas surgiram pela primeira vez na WWDC 2025 e são fornecidas com o Xcode 26+. ↩↩