Guia

Como automatizar testes de interface do iOS com XCUITest em um Mac dedicado

Para automatizar o XCUITest, rode xcodebuild test com um destino de simulador e um -resultBundlePath, em um Mac com uma sessão de usuário logada. Crie um simulador dedicado com xcrun simctl. Adicione -parallel-testing-enabled YES para dividir os testes entre clones do simulador, e -retry-tests-on-failure para repetir testes instáveis. Rodamos cada comando desta página em um Mac com Xcode 26.6 e o simulador do iOS 26.5.

O que você precisa antes

  • Um Mac com Xcode e pelo menos um runtime de simulador iOS.
  • Um target de teste de interface no projeto e um scheme compartilhado que o teste.
  • Uma sessão de área de trabalho logada. Testes de interface controlam o app Simulator, então o que roda esses testes precisa estar nessa sessão. Isso quer dizer um agente de CI iniciado como LaunchAgent, não como LaunchDaemon. O modelo de plist do Buildkite diz o mesmo: o modo GUI permite testes de interface do Xcode, mas precisa de um login.

1. Confira o Xcode e os runtimes

xcodebuild -version
sudo xcodebuild -runFirstLaunch
xcrun simctl list runtimes
xcrun simctl list devicetypes | grep iPhone

O -runFirstLaunch instala pacotes e aceita a licença. Rode depois de cada instalação ou atualização do Xcode.

2. Crie um simulador só para o CI

Um dispositivo dedicado separa o CI de qualquer coisa que uma pessoa abriu manualmente. O bootstatus -b inicia o simulador e espera até ele ficar pronto.

UDID=$(xcrun simctl create "CI iPhone 17" "iPhone 17" com.apple.CoreSimulator.SimRuntime.iOS-26-5)
xcrun simctl bootstatus "$UDID" -b
echo "$UDID"

Use o UDID no destino: -destination "platform=iOS Simulator,id=$UDID". Um nome também funciona, como name=iPhone 17,OS=26.5, mas dois dispositivos podem ter o mesmo nome.

3. Rode os testes de interface com um result bundle

rm -rf build/TestResults.xcresult
xcodebuild test \
  -project MyApp.xcodeproj \
  -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" \
  -derivedDataPath build/DerivedData \
  -resultBundlePath build/TestResults.xcresult

O result bundle guarda cada resultado de teste, log, captura de tela e falha. Leia um resumo na linha de comando:

xcrun xcresulttool get test-results summary --path build/TestResults.xcresult

Ele imprime um JSON com as contagens de aprovados, reprovados e pulados por dispositivo. Anexe o bundle à execução do CI como artefato, compactado com ditto -c -k --keepParent.

4. Faça o build uma vez e teste várias vezes

Separe o build da execução dos testes. Assim você pode repetir uma parte sem compilar de novo.

xcodebuild build-for-testing -project MyApp.xcodeproj -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" -derivedDataPath build/DerivedData

xcodebuild test-without-building -project MyApp.xcodeproj -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" -derivedDataPath build/DerivedData \
  -only-testing:MyAppUITests/LoginTests/testSignIn

O -only-testing aceita Target, Target/Class ou Target/Class/method. O -skip-testing funciona do mesmo jeito, ao contrário.

5. Rode testes em paralelo

xcodebuild test -project MyApp.xcodeproj -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" \
  -parallel-testing-enabled YES \
  -parallel-testing-worker-count 2 \
  -resultBundlePath build/TestResults.xcresult

O Xcode clona o simulador e distribui as classes de teste entre os clones. Nosso log mostrou testes rodando no Clone 1 of iPhone 17. -parallel-testing-enabled substitui a configuração do scheme. Em um Mac de 16 GB, comece com 2 workers e meça antes de aumentar. Cada clone é um simulador completo na memória. Para testar em vários tipos de dispositivo ao mesmo tempo, liste vários destinos e defina -maximum-concurrent-test-simulator-destinations.

6. Repita testes instáveis

xcodebuild test -project MyApp.xcodeproj -scheme MyApp \
  -destination "platform=iOS Simulator,id=$UDID" \
  -retry-tests-on-failure \
  -test-iterations 3 \
  -resultBundlePath build/TestResults.xcresult

O xcodebuild -help diz que um teste que falhou roda de novo até o número de iterações. Sem -test-iterations, o máximo é 3. Conferimos com um teste que falha uma vez e depois passa. A execução terminou em TEST SUCCEEDED. O resumo informou 3 execuções para 2 testes.

Então uma nova tentativa deixa um teste instável verde, e a instabilidade fica escondida. Leia o result bundle em toda execução e acompanhe quais testes precisaram de nova tentativa. Adicione -test-repetition-relaunch-enabled YES para rodar cada tentativa em um processo novo. Para caçar uma instabilidade, use -run-tests-until-failure. Ele não pode ser combinado com -retry-tests-on-failure.

7. Coloque limites em testes travados

-test-timeouts-enabled YES \
-default-test-execution-time-allowance 120 \
-maximum-test-execution-time-allowance 300

Adicione essas flags ao comando de teste. Um teste de interface que espera para sempre por um elemento passa a falhar depois do tempo permitido, e não segura a máquina até o timeout do CI.

8. Mantenha os simuladores limpos entre execuções

xcrun simctl shutdown "$UDID"
xcrun simctl erase "$UDID"
xcrun simctl delete unavailable

O erase restaura o conteúdo e os ajustes do simulador. O delete unavailable remove dispositivos que o Xcode atual não suporta mais. Rode depois de cada atualização do Xcode.

Como isso sobrevive a reinicializações

Os simuladores que você cria continuam lá depois de reinicializar. O que precisa voltar é o agente de CI que roda os testes. Rode como LaunchAgent, ative o login automático e inicie o simulador do CI no começo de cada job com bootstatus -b. Esse comando é seguro em um dispositivo que já está ligado.

Erros que encontramos e como resolver

  • Unable to find a device matching the provided destination specifier. O nome ou o OS não existe. Confira xcrun simctl list devices e a lista de runtimes.
  • xcodebuild: error: Existing file at -resultBundlePath. Apague o bundle antigo antes de cada execução.
  • Unable to erase contents and settings in current state: Booted. Desligue o simulador antes de apagá-lo.

Por que um Mac dedicado ajuda

Testes de interface gastam muito tempo antes do primeiro toque. Eles esperam o build, a inicialização do simulador e a instalação do app. Uma máquina que fica ligada mantém o DerivedData e um simulador iniciado prontos. No nosso benchmark fizemos o build do app iOS da Wikipedia com o Xcode 26.6. Um job depois de uma pequena mudança levou 27 segundos em um M6 aquecido. Ele levou 269 segundos em um runner macos-26 novo hospedado pelo GitHub. Nosso M6 tem 12 núcleos de CPU, então dois clones em paralelo ainda deixam espaço para o build.

Quando o CI hospedado basta

Uma suíte pequena rodada em poucos pull requests por dia cabe em minutos cobrados por uso. Com a tarifa do GitHub de $0.062 por minuto de macOS (conferida em setembro de 2026), um Mac de $139se paga depois de cerca de 2,242 minutos por mês. Abaixo disso, fique no hospedado. Se você precisa testar em muitos iPhones físicos, precisa de uma nuvem de dispositivos. Um Mac mini roda simuladores. A sua suíte em paralelo precisa de mais de 16 GB de memória? Nossos planos M5 Pro têm 48 ou 64 GB, mas são pré-venda, então conte com uma espera de cerca de uma semana.

O guia de pipeline de CI/CD para iOS mostra onde os testes de interface entram em um pipeline completo. A calculadora faz as contas com os seus números.

Perguntas frequentes

O xcodebuild tem uma flag para repetir testes que falharam?

+

Sim. O -retry-tests-on-failure roda de novo um teste que falhou, até -test-iterations vezes, ou 3 por padrão. Ele não pode ser combinado com -run-tests-until-failure.

Como rodo testes do XCUITest em paralelo pela linha de comando?

+

Adicione -parallel-testing-enabled YES e, se quiser, -parallel-testing-worker-count. O Xcode clona o simulador e divide as classes de teste entre os clones.

O XCUITest roda em um Mac sem monitor?

+

Ele precisa de uma sessão de usuário logada, porque os testes de interface controlam o app Simulator. Rode o agente de CI como LaunchAgent e ative o login automático.

Como leio os resultados do xcodebuild test sem abrir o Xcode?

+

Passe -resultBundlePath e depois rode xcrun xcresulttool get test-results summary --path no bundle. Ele imprime as contagens em JSON.

Guias relacionados