Você já se acostumou a escrever código com async/await, mas já ficou sem saber por onde começar quando chegou a hora de escrever os testes?
Isso acontece principalmente quando ainda resta o hábito de testar o inferno de callbacks adicionando XCTestExpectation a wait(for:).
Vamos direto ao ponto. Com Swift 5.5 ou posterior (Xcode 13+), declare a própria função de teste ** como async, aguarde o resultado com await e faça a validação com **. A abordagem antiga com expectations e timeouts deixou de ser necessária na maioria dos casos.
Hoje vou explicar, do início ao fim, as formas de testar async/await que organizei na prática profissional.
Resumo dos pontos principais
Para quem está com pressa, estes são os pontos essenciais do artigo.
- Adicione
async throwsao método de teste e chameawaitdentro dele - Para validar erros, use
do-catchou o padrão da versão async deXCTAssertThrowsError - Use
expectationapenas quando um timeout for realmente necessário - Envolva APIs antigas de callback com
withCheckedThrowingContinuationpara testá-las
Como testar async/await no Swift?
Vamos começar pelo formato mais básico.
Antes, criávamos uma expectation para aguardar o resultado assíncrono e chamávamos fulfill dentro de uma closure. O código ficava longo e difícil de ler.
Agora ficou muito mais conciso.
// Basta adicionar async throws à função de teste
func test_Carregar usuário_() async throws {
let service = UserService()
let user = try await service.fetchUser(id: 1)
XCTAssertEqual(user.name, "Lee Seok-woo")
}
Há exatamente dois pontos para observar no código acima.
Primeiro, async throws foi adicionado à declaração da função. Segundo, depois de aguardar o resultado com try await, fazemos a validação normalmente com XCTAssertEqual.
Ao tornar a própria função de teste async, o código assíncrono passa a ser lido de cima para baixo como código síncrono.
Depois que mudei para essa abordagem, reduzi quase pela metade o número de linhas do código de teste.
Como validar os casos em que ocorre um erro?
Também é comum testar situações em que uma função assíncrona lança um erro.
A forma mais intuitiva é do-catch.
func test_Usuário_ inexistente_ erro() async {
let service = UserService()
do {
_ = try await service.fetchUser(id: -1)
XCTFail("O correto é ocorrer um erro")
} catch {
XCTAssertTrue(error is UserError)
}
}
O ponto principal é adicionar XCTFail ao caso de sucesso.
Se nenhum erro ocorrer e o teste simplesmente passar, pode parecer que ele foi bem-sucedido silenciosamente. Pense nisso como uma proteção contra esse problema.
A propósito, XCTAssertThrowsError usado no código síncrono não consegue receber uma função async diretamente por padrão. Por isso, costumo escrevê-lo explicitamente com do-catch, como acima.
Como testar APIs antigas baseadas em callback?
Seria ótimo se todo o código já tivesse migrado para async, mas a realidade é outra.
O projeto provavelmente ainda tem APIs antigas que retornam resultados por meio de completion handlers.
Nesse caso, envolva-as com withCheckedThrowingContinuation e leve-as para o mundo async.
func fetchLegacy() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
oldAPI { data, error in
if let error { continuation.resume(throwing: error) }
else { continuation.resume(returning: data!) }
}
}
}
Depois de envolvê-las, no teste basta chamar com try await fetchLegacy(), exatamente como na abordagem anterior.
Um cuidado importante: uma continuation deve resume exatamente uma vez. Chamá-la duas vezes causa um crash; não chamá-la faz o teste travar para sempre.
Quando usar expectations e quando usar testes async
Comparei as duas abordagens. (Xcode 13 ou posterior, em 2026)
| Item | Abordagem com expectation | Abordagem de teste async |
|---|---|---|
| Tamanho do código | Longo | Curto |
| Legibilidade | Callbacks aninhados | Sequencial, de cima para baixo |
| Definição de timeout | Fácil | Requer tratamento separado |
| Situação recomendada | Aguardar notificações ou timers | A maioria das operações assíncronas |
Para testes comuns de funções async/await, a abordagem async é muito mais conveniente.
Porém, quando um timeout explícito é importante—por exemplo, para verificar se uma notificação chega dentro de um período específico ou se um timer funciona no momento certo—continua sendo útil usar expectation em conjunto.
Resumo
Mesmo que pareça estranho no início, basta adicionar async throws uma vez à função de teste para você se perguntar por que não mudou antes.
Depois de dominar os quatro padrões de hoje, você conseguirá cobrir a maioria dos testes assíncronos sem dificuldade. Aplique um de cada vez; estou torcendo por você!

