Ao migrar testes unitários do XCTest no Mac, preserve a sequência de propriedade: conclua o trabalho, libere as referências fortes pretendidas e, em seguida, verifique a referência fraca. Para uma limpeza síncrona, um defer local ao teste deixa essa sequência explícita.

Este passo a passo se concentra na conversão de um teste unitário sensível à propriedade no Xcode. O exemplo calcula o número de células em uma grade sintética e, em seguida, verifica se o calculador foi liberado. Ele usa apenas objetos locais do Swift e operações aritméticas com inteiros.

Que comportamento do ciclo de vida a migração precisa preservar?

O XCTest oferece uma sequência gerenciada pelo framework. Após o término de um método de teste, ele executa os blocos de teardown registrados na ordem inversa à do registro, seguidos pelos métodos de teardown da instância. A limpeza registrada ainda é executada quando uma asserção do teste falha; o encerramento do processo pode impedi-la. Esses detalhes são importantes quando um bloco libera um objeto e outro verifica sua referência fraca. Configurar e desmontar o estado nos testes

Por exemplo, um bloco de teardown que verifica nil precisa ser executado depois do bloco que remove o último proprietário pretendido. Como o XCTest usa a ordem inversa à do registro, a verificação é registrada primeiro e a liberação, depois. Inverter esses registros muda o que o teste observa.

O guia de migração do Swift Testing associa a configuração a um inicializador e o teardown a um deinicializador em uma suíte do tipo classe ou ator. Esse mapeamento é útil para a limpeza associada ao ciclo de vida da suíte. Uma asserção de propriedade exige uma decisão adicional: qual referência exatamente deve desaparecer antes de a asserção ser executada? Migrar um teste do XCTest

Use esta tabela de decisão antes de alterar a sintaxe do teste:

Responsabilidade existente Escolha de migração Motivo
Construir um estado inicial novo para cada teste de instância Inicialização de propriedade armazenada ou init() Estabelece o estado inicial antes de o teste ser executado
Liberar um objeto e inspecionar imediatamente uma referência fraca Liberação explícita seguida de #expect, geralmente dentro de um defer local Torna visível o ponto de observação
Fazer uma limpeza síncrona ligada ao ciclo de vida de uma suíte de tipo referência Suíte do tipo classe ou ator com deinit Segue o mapeamento documentado do ciclo de vida
Aguardar a limpeza ou relatar um erro de limpeza Limpeza aguardada em uma função assíncrona ou um escopo personalizado apropriado Exige um fluxo de execução que permita suspensão e tratamento de erros

A tabela é um recurso de apoio à decisão de migração, derivado das regras documentadas de ciclo de vida e da linguagem. Ela não implica que todo hook do XCTest tenha um método equivalente no Swift Testing.

Por que mover a verificação da referência fraca para deinit pode alterar o teste?

O Swift chama um deinicializador de classe imediatamente antes de desalocar a instância. O deinicializador ainda pode acessar as propriedades armazenadas dessa instância. Portanto, entrar no deinit de uma suíte não prova, por si só, que um objeto mantido por uma dessas propriedades já foi liberado. Deinicialização

Considere uma suíte com uma propriedade forte layout. Uma referência fraca a esse layout pode continuar diferente de nil enquanto o deinicializador da suíte está sendo executado. O teste precisa remover a referência forte da propriedade antes de verificar se o layout foi liberado.

Há outra possibilidade de falha: um ciclo de referências que retém a suíte pode impedir que seu deinicializador seja executado. Nesse caso, uma asserção colocada somente nesse deinicializador não terá a oportunidade de informar a retenção. Isso decorre das regras de propriedade do ARC: referências fortes mantêm as instâncias vivas, enquanto referências fracas não impedem a desalocação e se tornam nil quando a instância referenciada é desalocada. Contagem automática de referências

Em um teste de propriedade, mantenha o observador fora do objeto cuja liberação está sendo testada. Dê ao teste uma operação específica de liberação e execute sua asserção pelo fluxo de controle normal do teste. Assim, o contrato do teste pode ser inspecionado sem depender da desalocação da própria suíte.

Estabeleça a linha de base do XCTest no Xcode

Use um destino de teste unitário existente no Xcode 16 ou posterior. A Apple documenta que XCTest e Swift Testing podem coexistir no mesmo destino e pacote; portanto, a migração não exige outro pacote de testes unitários. Para adicionar um arquivo de teste, use File > New > New File From Template e selecione XCTest Unit Test ou Swift Testing Unit Test, conforme apropriado. Adicionar testes ao seu projeto do Xcode

A seguir está um arquivo de teste XCTest completo. Seu calculador retorna 3 * 4, produzindo o resultado esperado de 12. Os dois registros de teardown expõem deliberadamente a ordem de execução necessária.

import XCTest

private final class GridCalculator {
    func cellCount(columns: Int, rows: Int) -> Int {
        columns * rows
    }
}

final class GridCalculatorXCTests: XCTestCase {
    private var calculator: GridCalculator?

    override func setUp() {
        super.setUp()
        calculator = GridCalculator()
    }

    func testCellCountAndRelease() {
        weak var observed = calculator

        // Registered first, so this executes second.
        addTeardownBlock { [weak observed] in
            XCTAssertNil(observed)
        }

        // Registered second, so this executes first.
        addTeardownBlock { [self] in
            calculator = nil
        }

        XCTAssertEqual(
            calculator?.cellCount(columns: 3, rows: 4),
            12
        )
    }
}

O bloco de liberação mantém o caso de teste retido para poder atualizar a propriedade do estado inicial. O bloco de observação captura o calculador como referência fraca, evitando um proprietário forte adicional na própria verificação. A Apple documenta os blocos de teardown como o local para a limpeza específica do teste registrada junto à configuração associada. Configurar e desmontar o estado nos testes

O contrato da linha de base é definido de forma restrita: depois que o corpo do teste termina e a propriedade do estado inicial é limpa, o calculador não deve mais existir. Isso não mede o consumo de memória nem estabelece que todos os objetos criados pelo aplicativo foram liberados.

Migre a sequência de propriedade para um escopo local

Para este caso síncrono, use uma suíte struct e coloque a liberação e a verificação em um único bloco defer. O Swift executa o código adiado quando o controle sai do escopo que o contém, inclusive em um retorno antecipado ou em um erro lançado. Vários blocos adiados são executados na ordem inversa; um único bloco facilita a leitura da sequência de liberação antes da verificação neste exemplo. Fluxo de controle

A seguir está um arquivo completo e independente do Swift Testing. Substitua o arquivo da linha de base por ele ou mantenha os exemplos em arquivos separados, onde os tipos auxiliares privados permaneçam separados. O segundo teste é um controle de diagnóstico: demonstra que um proprietário forte adicional mantém a referência fraca preenchida até que esse proprietário seja liberado.

import Testing

private final class GridCalculator {
    func cellCount(columns: Int, rows: Int) -> Int {
        columns * rows
    }
}

private final class GridFixture {
    var calculator: GridCalculator? = GridCalculator()
    var additionalOwner: GridCalculator?
}

struct GridCalculatorTests {
    @Test
    func cellCountAndRelease() throws {
        let fixture = GridFixture()
        weak var observed = fixture.calculator

        defer {
            fixture.calculator = nil
            #expect(observed == nil)
        }

        let result = try #require(
            fixture.calculator?.cellCount(columns: 3, rows: 4)
        )
        #expect(result == 12)
    }

    @Test
    func weakProbeDetectsAnAdditionalOwner() {
        let fixture = GridFixture()
        weak var observed = fixture.calculator

        fixture.additionalOwner = fixture.calculator
        fixture.calculator = nil
        #expect(observed != nil)

        fixture.additionalOwner = nil
        #expect(observed == nil)
    }
}

No primeiro teste, o estado inicial possui o calculador, e observed o observa sem possuí-lo. O cálculo produz um inteiro opcional, que #require desembrulha. Nenhuma instância desembrulhada do calculador é armazenada em outra variável local. Ao sair do escopo, o bloco adiado limpa a referência do estado inicial e verifica o observador.

Um #expect que falha registra um problema e permite que a execução continue. Um #require que falha lança um erro, interrompendo esse caminho de execução. Como o defer é registrado antes do requisito, sua limpeza e verificação da referência fraca ainda são executadas quando o requisito lança um erro. Expectativas e confirmações, Tratamento de erros

O controle de diagnóstico estabelece dois estados esperados distintos: limpar um dos dois proprietários deixa o observador diferente de nil; limpar o proprietário restante o deixa como nil. São verificações ilustrativas de propriedade, não resultados medidos do aplicativo. Os exemplos em Swift foram inspecionados, mas não puderam ser compilados no ambiente de pesquisa porque não havia um executável do Swift disponível.

Verifique os limites dos auxiliares, a concorrência e a limpeza assíncrona

Um defer local pertence ao escopo que o contém. Mover o bloco adiado para uma função auxiliar de fábrica faz com que ele seja executado quando essa função retorna, o que pode acontecer antes de o teste usar o objeto retornado. Mantenha o bloco no teste ou crie um auxiliar que execute toda a operação dentro do próprio escopo. O escopo que contém o bloco faz parte do contrato de propriedade. Fluxo de controle

Verifique também onde fica o estado inicial. O Swift Testing cria uma instância de suíte distinta para cada método de teste de instância, e os testes de uma suíte são executados em paralelo por padrão. O estado local da instância ou do teste se ajusta a esse modelo; um estado inicial global ou uma coleção estática introduz propriedade compartilhada entre os testes. Organizar funções de teste com tipos de suíte

Para código de aplicativo Mac que exige o ator principal, isole explicitamente o teste do Swift Testing com @MainActor. O guia de migração observa que o Swift Testing não herda o comportamento padrão de ator principal do XCTest para métodos de teste síncronos. O calculador sintético acima não tem esse requisito. Migrar um teste do XCTest

O bloco síncrono adiado não substitui um teardown aguardado. As ações adiadas do Swift não podem lançar um erro para fora do bloco. Se a limpeza puder falhar, trate e informe essa falha explicitamente em vez de descartá-la. Mantenha a limpeza aguardada em um fluxo de execução assíncrono e garanta que os caminhos do corpo que terminam com sucesso e os que lançam erros cheguem a ela. Tratamento de erros

Para configuração e teardown reutilizáveis e gerenciados pelo framework, o Swift Testing atual fornece TestScoping, que oferece execução personalizada em torno de um teste ou suíte. A Apple lista sua disponibilidade com Swift 6.1 e Xcode 16.3. É uma opção compatível quando vários testes precisam de um escopo comum, mas exige um trait personalizado e um provedor de escopo, em vez de simplesmente renomear addTeardownBlock. TestScoping

Diagnostique uma verificação de referência fraca que muda após a migração

Se o teste de propriedade migrado falhar, inspecione estes pontos nesta ordem:

  1. Confirme o ponto de observação. A liberação pretendida precisa acontecer antes da asserção da referência fraca.
  2. Inspecione as propriedades do estado inicial. Uma propriedade restante ainda pode possuir o calculador depois que outra propriedade é limpa.
  3. Inspecione aliases locais e capturas de closures. Um objeto desembrulhado ou uma referência capturada fortemente pode prolongar a propriedade até a verificação.
  4. Inspecione o escopo do auxiliar. A limpeza adiada de um auxiliar pode ser executada cedo demais para o contrato pretendido pelo teste.
  5. Inspecione o trabalho pendente. Se o objeto real iniciar trabalho assíncrono, defina e aguarde sua conclusão antes de aplicar uma expectativa de liberação síncrona.

Uma referência fraca diferente de nil identifica um objeto que ainda existe no ponto de observação. Investigue o proprietário restante antes de alterar a asserção ou adicionar um atraso. Neste exemplo sintético, não há trabalho assíncrono a aguardar, e o controle de diagnóstico mostra exatamente como um proprietário adicional afeta o resultado.

A migração está concluída quando os cálculos e as asserções de propriedade expressam o mesmo resultado pretendido da linha de base: calcular 12, remover a propriedade pretendida do estado inicial e verificar a liberação por meio de uma referência fraca mantida independentemente. Mantenha essa sequência visível à medida que o teste evolui.

Fontes