Confesso: durante anos eu olhei para o XCTest e pensei "tá bom, funciona". Aí veio o Swift Testing, o novo framework oficial da Apple, e eu finalmente entendi o que estava perdendo. Apresentado na WWDC 2024 e amadurecido com o Swift 6.2 e o Xcode 26, ele troca décadas de herança do Objective-C por uma API moderna baseada em macros — com suporte nativo a Swift Concurrency, testes parametrizados e traits reutilizáveis. Neste guia, vamos do zero: o que mudou, por que importa, e como migrar suítes existentes sem detonar seu pipeline de CI.
Swift Testing no Xcode 26: Guia Completo para Migrar do XCTest
Guia prático para 2026 do framework Swift Testing no Xcode 26: macros @Test, #expect e #require, testes parametrizados, traits, tags, confirmation, exit tests e migração incremental do XCTest sem quebrar o CI.

O XCTest nasceu na era do Objective-C. Sua arquitetura depende de herança obrigatória de XCTestCase, detecção de testes pelo prefixo test no nome do método e dezenas de variações de XCTAssertEqual, XCTAssertTrue, XCTAssertNil e companhia. Funciona? Funciona. Mas combina pouco com o jeito que escrevemos Swift hoje: structs, actors, async/await, macros.
O Swift Testing resolve esses pontos com cinco decisões de design bem deliberadas:
- Macros em vez de prefixos: qualquer função marcada com
@Testé um teste, independentemente do nome. - Suítes flexíveis: você pode usar
struct,classouactor— a herança obrigatória sumiu. - Uma única forma de asserção:
#expecte#requiresubstituem dezenas de funçõesXCTAssert*. - Concorrência nativa: testes async são suportados de forma natural, e o framework executa em paralelo dentro do mesmo processo.
- Traits componíveis: tags, condições, limites de tempo, ordem de execução — tudo aplicado como modificadores declarativos.
E uma boa notícia logo de cara: o XCTest não está obsoleto. Os dois frameworks rodam lado a lado no mesmo target de testes, o que torna a migração incremental e (importante) segura.
Configurando o ambiente no Xcode 26
O Swift Testing já vem incluído no Xcode 16 e nas versões posteriores. No Xcode 26, ao criar um novo target de testes (File > New > Target > Unit Testing Bundle), o template padrão já gera um arquivo usando @Test. Para projetos existentes, basta importar:
import Testing
@testable import MeuApp
Em pacotes Package.swift com swift-tools-version:6.0 ou superior, nenhuma dependência adicional é necessária — o módulo Testing faz parte do toolchain. É plug-and-play, basicamente.
Seu primeiro teste com @Test
O modelo mental mais simples possível: escreva uma função, anote-a com @Test e use #expect para verificar resultados. Só isso.
import Testing
@testable import Calculadora
@Test func somaDoisNumerosPositivos() {
let resultado = Calculadora.somar(2, 3)
#expect(resultado == 5)
}
Quando o teste falha, o Swift Testing exibe os valores reais envolvidos na expressão graças à expansão de macros. Na prática, isso elimina aquela dor antiga de escrever mensagens manuais como XCTAssertEqual(a, b, "esperava \(a) igual a \(b)") — o framework já mostra tudo pra você.
Nomeando testes com clareza
O argumento opcional do macro permite títulos legíveis no Test Navigator (e seus colegas vão te agradecer):
@Test("Soma de dois inteiros positivos retorna o valor esperado")
func somaDoisNumerosPositivos() {
#expect(Calculadora.somar(2, 3) == 5)
}
Agrupando testes em Suites
Uma suite é um tipo (struct, class ou actor) anotado com @Suite. A Apple recomenda começar sempre com struct, exceto quando você precisa de deinit para limpeza:
@Suite("Calculadora — operações básicas")
struct CalculadoraTests {
let calc = Calculadora()
@Test func somaFunciona() {
#expect(calc.somar(2, 3) == 5)
}
@Test func subtracaoFunciona() {
#expect(calc.subtrair(10, 4) == 6)
}
}
Detalhe importante: cada teste recebe uma nova instância da suite. Ou seja, let calc = Calculadora() roda uma vez por teste, garantindo isolamento sem código de boilerplate. É lindo de ver.
Setup e teardown
O setup acontece no init da suite. O teardown, quando necessário, vai no deinit de uma class ou actor:
@Suite final class BancoDeDadosTests {
let db: BancoDeDados
init() async throws {
db = try await BancoDeDados.emMemoria()
try await db.popularComDadosDeTeste()
}
deinit {
try? db.fechar()
}
@Test func consultaUsuarioPorId() async throws {
let usuario = try await db.usuario(id: 1)
#expect(usuario.nome == "Ana")
}
}
#expect e #require: a nova era das asserções
Em vez de memorizar dezenas de variantes de XCTAssert, você usa apenas duas macros. Sério, duas:
#expect(condição)— registra a falha mas continua executando o teste.#require(condição)— falha e interrompe o teste imediatamente. Útil para pré-condições críticas.
@Test func processaPedidoValido() throws {
let pedido = try #require(repositorio.pedido(id: 42))
#expect(pedido.total > 0)
#expect(pedido.itens.count == 3)
#expect(pedido.cliente.email.contains("@"))
}
Repare que #require com um Optional faz unwrap seguro: se for nil, o teste para; caso contrário, o valor é retornado já desembrulhado. Dois pássaros, uma macro.
Verificando erros lançados
@Test func divisaoPorZeroLancaErro() {
#expect(throws: Calculadora.Erro.divisaoPorZero) {
try Calculadora.dividir(10, por: 0)
}
}
Testes parametrizados: menos código, mais cobertura
Honestamente, esse talvez seja o maior salto produtivo em relação ao XCTest. Você passa uma sequência de valores e o framework executa o teste para cada um — em paralelo:
@Test(arguments: [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300)
])
func somaProduzResultadoCorreto(a: Int, b: Int, esperado: Int) {
#expect(Calculadora.somar(a, b) == esperado)
}
O Test Navigator do Xcode 26 mostra cada caso individualmente, com a possibilidade de re-executar somente os que falharam (uma mão na roda quando você tem 200 casos e só 2 quebraram). Para combinar duas dimensões — produto cartesiano:
@Test(arguments: ["BRL", "USD", "EUR"], [10.0, 100.0, 1000.0])
func formatadorMoedaProduzStringValida(moeda: String, valor: Double) {
let resultado = Formatador.formatar(valor, moeda: moeda)
#expect(!resultado.isEmpty)
}
Traits: customizando o comportamento dos testes
Traits são modificadores declarativos aplicados ao @Test ou @Suite. Os mais úteis no dia a dia:
Desabilitando ou ignorando temporariamente
@Test(.disabled("Aguardando correção do bug #1247"))
func recursoQuebrado() { /* ... */ }
Limitando por sistema operacional
@Test(.enabled(if: ProcessInfo.processInfo.isOperatingSystemAtLeast(
.init(majorVersion: 26, minorVersion: 0, patchVersion: 0)
)))
func recursoExclusivoIOS26() { /* ... */ }
Definindo limite de tempo
@Test(.timeLimit(.minutes(1)))
func operacaoLonga() async throws { /* ... */ }
Forçando execução serial
Por padrão, todos os testes rodam em paralelo. Se uma suite acessa um recurso compartilhado (arquivo, banco em disco, mock global), aplique .serialized:
@Suite(.serialized)
struct TestesQueAcessamArquivoCompartilhado {
@Test func escreveELe() throws { /* ... */ }
@Test func sobrescreveEverifica() throws { /* ... */ }
}
Tags: organizando testes por categoria
Tags são uma forma de agrupar testes que cruzam fronteiras de suites. Defina-as em uma extensão de Tag:
extension Tag {
@Tag static var rede: Self
@Tag static var lento: Self
@Tag static var critico: Self
}
Depois é só aplicar como trait:
@Test(.tags(.rede, .lento))
func sincronizaComServidorRemoto() async throws { /* ... */ }
@Test(.tags(.critico))
func calculoFinanceiroExato() { /* ... */ }
No Xcode 26, a aba Tags do Test Navigator permite filtrar e executar testes por tag. Em CI, você pode rodar apenas tags específicas com xcodebuild test -only-testing-tag — útil para criar pipelines rápidos (sem a tag lento) e pipelines completos (todas as tags). No último projeto que trabalhei, isso cortou o tempo de feedback do PR de 14 para 3 minutos.
Confirmation: testando eventos assíncronos
O tipo Confirmation substitui XCTestExpectation de forma muito mais fluida. Ele confirma que um evento ocorre um número específico de vezes durante a execução de um bloco:
@Test func notificaObservadoresQuandoEstadoMuda() async {
let store = Store()
await confirmation("Observador foi chamado", expectedCount: 1) { confirmar in
store.observar { _ in
confirmar()
}
store.atualizarEstado(.novo)
}
}
No Swift 6.2, o expectedCount aceita RangeExpression, o que permite verificações como "entre 2 e 5 chamadas":
await confirmation("Retry executado", expectedCount: 2...5) { confirmar in
await servico.executarComRetry { confirmar() }
}
Exit tests: validando crashes esperados
Novidade do Swift 6.2 que eu queria há anos: exit tests. Eles permitem verificar que um determinado código encerra o processo (por fatalError, precondition, etc.). Antes, era praticamente impossível testar isso sem hacks bizarros.
@Test func acessoForaDosLimitesProvocaCrash() async {
await #expect(processExitsWith: .failure) {
var array: [Int] = []
_ = array[10]
}
}
Migrando uma suite XCTest para Swift Testing
O processo é mecânico para a maioria dos casos. Considere este teste XCTest típico:
import XCTest
@testable import Carrinho
final class CarrinhoTests: XCTestCase {
var carrinho: Carrinho!
override func setUp() {
super.setUp()
carrinho = Carrinho()
}
override func tearDown() {
carrinho = nil
super.tearDown()
}
func testAdicionarItemAumentaTotal() {
carrinho.adicionar(Item(preco: 10))
XCTAssertEqual(carrinho.total, 10)
}
func testRemoverItemInexistenteFalha() {
XCTAssertThrowsError(try carrinho.remover(id: "nao-existe"))
}
}
E aqui a versão equivalente em Swift Testing — repare como o código simplesmente encolhe:
import Testing
@testable import Carrinho
@Suite struct CarrinhoTests {
let carrinho = Carrinho()
@Test func adicionarItemAumentaTotal() {
carrinho.adicionar(Item(preco: 10))
#expect(carrinho.total == 10)
}
@Test func removerItemInexistenteFalha() {
#expect(throws: (any Error).self) {
try carrinho.remover(id: "nao-existe")
}
}
}
Checklist de migração
- Troque
import XCTestporimport Testing. - Remova a herança de
XCTestCasee prefirastruct. - Mova o conteúdo de
setUp()para oinit; removatearDown()ou mova paradeinit. - Anote cada método com
@Teste remova o prefixotestdo nome. - Substitua
XCTAssert*por#expectou#require. - Substitua
XCTestExpectationporconfirmation. - Avalie consolidar testes repetitivos com
@Test(arguments:).
O que não migrar (ainda)
Algumas categorias permanecem exclusivas do XCTest no Xcode 26:
- Testes de UI:
XCUIApplication,XCUIElemente gravações de UI continuam sendo XCTest. Mantenha esses testes em arquivos separados. - Testes de performance:
XCTMetric,XCTMemoryMetricemeasure { }ainda não têm equivalente em Swift Testing.
Regra de ouro (e levei um tombo até aprender): nunca misture XCTAssert com #expect dentro de uma mesma função de teste. Os dois frameworks coexistem em targets, mas não dentro de um mesmo método.
Integração com CI/CD no Xcode 26
Para rodar Swift Testing via linha de comando:
# Pacote SwiftPM
swift test
# Projeto Xcode
xcodebuild test \
-scheme MeuApp \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'
Para filtrar por tag em CI:
xcodebuild test \
-scheme MeuApp \
-only-testing-tag critico \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'
O Swift Testing roda em paralelo dentro do mesmo processo via Swift Concurrency, ao contrário do XCTest, que abre múltiplas instâncias de Simulator. O resultado prático: builds 2x a 5x mais rápidos em projetos com muitos testes, e ainda dá pra paralelizar em dispositivos físicos.
Boas práticas adotadas pela comunidade em 2026
- Centralize tags em um único arquivo (tipo
TestTags.swift) para facilitar descoberta e evitar duplicação. - Extraia traits reutilizáveis em um diretório próprio. Custom traits (Swift 6.1+) permitem encapsular setup/teardown que antes vivia em hierarquias de classes.
- Mocks e fixtures em arquivos separados dos testes, para que cada arquivo de teste fique focado em comportamento.
- Prefira parametrização a múltiplos testes copiados: reduz código e melhora os relatórios de falha.
- Use
.serializedapenas quando estritamente necessário — o paralelismo é uma das maiores vantagens do framework. - Não migre tudo de uma vez: XCTest e Swift Testing convivem. Comece pelos novos testes e migre módulos antigos quando tocar neles. Quem tentou o "big bang" se arrependeu.
Perguntas frequentes
O Swift Testing substitui completamente o XCTest?
Não. O XCTest continua suportado e é o único caminho oficial para testes de UI (XCUIApplication) e medições de performance (XCTMetric). Para testes unitários e de integração em código Swift, o Swift Testing é a recomendação atual da Apple, mas a transição pode ser gradual — os dois frameworks rodam no mesmo target.
Preciso do Xcode 26 para usar Swift Testing?
Não. O Swift Testing está disponível desde o Xcode 16 e Swift 6.0. O Xcode 26 traz melhorias como exit tests, ranged confirmations e novos scoping traits, mas o conjunto principal de funcionalidades já existia em versões anteriores. Para projetos open-source, ele também funciona em Linux e Windows via swift-testing.
Como rodar apenas alguns testes específicos?
Você pode usar tags (xcodebuild test -only-testing-tag minhaTag), filtrar por nome qualificado (xcodebuild test -only-testing:MeuTarget/CarrinhoTests/adicionarItem) ou usar a interface gráfica do Test Navigator no Xcode, que ganhou no Xcode 26 uma aba dedicada para tags.
Qual a diferença entre #expect e #require?
#expect registra a falha e continua executando o restante do teste — útil quando você quer ver todas as falhas de uma vez. #require falha e interrompe imediatamente o teste, sendo a escolha correta para pré-condições. Quando aplicado a um Optional, #require também faz unwrap e retorna o valor desembrulhado.
Posso testar código com async/await sem usar XCTestExpectation?
Sim, e é muito mais limpo. Funções de teste podem ser async diretamente: @Test func minhaFunc() async throws { ... }. Para verificar que um callback é chamado N vezes durante uma operação assíncrona, use a API confirmation, que substitui XCTestExpectation com sintaxe declarativa e suporte a range expressions no Swift 6.2.
Conclusão
O Swift Testing não é só uma "versão moderna" do XCTest — é uma reformulação completa de como testes deveriam ser escritos em Swift desde o início. Macros eliminam boilerplate, parametrização reduz duplicação, traits oferecem composição flexível, e o suporte nativo a concurrency torna testes assíncronos triviais.
Com Xcode 26 e Swift 6.2, o framework está maduro o suficiente para ser a escolha padrão em qualquer projeto novo. Para projetos existentes, a migração incremental é segura: adicione import Testing, escreva os próximos testes na nova sintaxe e migre o código legado quando tocar nele. Em 2026, ainda escrever XCTAssertEqual em código novo é, no mínimo, um sinal de que vale a pena revisitar suas práticas de teste. Bons testes!


