Para que os testes XCTest passem individualmente e em conjunto, forneça a cada teste sua própria fixture mutável e injete-a no código em teste. Quando um singleton precisar continuar compartilhado, redefina-o antes de cada teste e impeça acessos simultâneos durante toda a execução do teste.

Este guia acompanha uma tarefa de desenvolvimento no Mac: corrigir testes de cálculo de layout no Xcode que passam isoladamente, mas falham quando executados em conjunto. O exemplo usa apenas larguras inteiras sintéticas e operações aritméticas locais. Ele não exige janela de aplicativo, mock de serviço, armazenamento em arquivos nem conexão de rede.

Por que um teste pode passar isoladamente e falhar em conjunto?

Um teste executado isoladamente pode encontrar o estado inicial de um singleton. Em uma suíte, um teste anterior pode já tê-lo alterado. Uma nova referência a esse singleton continua apontando para o mesmo objeto mutável.

O Swift distingue propriedades de instância de propriedades de tipo: cada instância tem suas próprias propriedades de instância, enquanto uma propriedade de tipo tem uma única cópia compartilhada. Propriedades de tipo armazenadas são inicializadas uma vez, no primeiro acesso. Portanto, atribuir SpacingCalculator.shared a uma propriedade de teste não cria uma calculadora nova. Consulte a documentação sobre propriedades do projeto Swift.

Considere uma linha sintética com itens cujas larguras são [10, 20, 30]. A largura total é a soma das larguras dos itens mais os espaços entre eles. Com um espaço padrão de 2, o resultado é 64; com um espaço de 8, é 76.

Suponha que um teste verifique a largura padrão sem configurar a calculadora, enquanto outro altera o espaço compartilhado para 8. A tabela a seguir descreve a execução sequencial, começando cada linha com uma calculadora nova cujo espaço é 2. São cálculos ilustrativos, não resultados registrados de testes no Xcode.

Execução Calculadora compartilhada sem redefinição Calculadora nova por teste
Teste padrão isolado 64: passa 64: passa
Teste de espaço amplo isolado 76: passa 76: passa
Padrão, depois espaço amplo Ambos passam Ambos passam
Espaço amplo, depois padrão O teste padrão recebe 76: falha Ambos passam

Isso fornece um diagnóstico útil: verifique qual teste altera o estado usado pela asserção que falha. Mudar os nomes dos testes para obter uma ordem favorável mantém a dependência.

Qual método do ciclo de vida do XCTest deve ser responsável pela fixture?

Use um método de configuração de instância para o estado mutável de que todos os testes precisam. A Apple documenta a configuração de instância antes de cada método de teste, enquanto a configuração no nível da classe é executada uma vez antes da classe de teste. Portanto, uma fixture mutável criada na configuração no nível da classe não recebe uma base nova entre os métodos. Consulte Configurar e encerrar o estado nos testes.

Escolha o método de configuração adequado ao trabalho: a criação síncrona e não lançadora de erros de uma fixture se encaixa em setUp(); a criação que lança erros se encaixa em setUpWithError(); a preparação assíncrona se encaixa em setUp() async throws. Use a finalização para concluir a limpeza do teste atual. A Apple recomenda explicitamente não depender da finalização para preparar os testes seguintes, pois uma falha pode impedir que ela seja executada. A mesma documentação do ciclo de vida descreve esses limites.

A decisão sobre a fixture depende tanto da propriedade quanto do escopo de execução:

Situação Abordagem adequada Condição a verificar
Os testes podem construir a dependência Crie e injete uma instância por teste O consumidor deve usar essa instância injetada
Os testes precisam exercitar o singleton real Estabeleça uma base completa antes de cada teste Nenhum teste simultâneo pode alterar esse singleton
A configuração compartilhada é imutável Reutilize o valor imutável Os objetos referenciados também devem permanecer inalterados
Um teste inicia trabalho assíncrono Aguarde a conclusão antes de encerrar o teste Nenhum trabalho pendente pode alterar a fixture depois disso

A primeira opção é a mais simples para esta calculadora. O código de produção pode manter um padrão compartilhado, enquanto os testes escolhem explicitamente instâncias independentes.

Crie uma fixture XCTest com propriedade independente

Use um Mac com Xcode e um destino de testes unitários macOS existente. Adicione o exemplo completo a seguir como um único arquivo Swift nesse destino de teste. Ele define sua própria calculadora, seu próprio consumidor e seus próprios testes, portanto não precisa importar um módulo de aplicativo. Use uma cadeia de ferramentas Swift 6 com o isolamento de ator padrão do destino definido como nonisolated.

A calculadora é um ator para tornar explícito o limite de concorrência do seu estado mutável. Atores Swift restringem o acesso ao estado mutável, e chamadas externas ao ator usam await. Essas regras estão documentadas no capítulo sobre concorrência do projeto Swift. A propriedade independente da fixture continua necessária, mesmo com um ator.

import XCTest

actor SpacingCalculator {
    static let shared = SpacingCalculator()

    private var gap = 2

    func setGap(_ value: Int) {
        precondition(value >= 0)
        gap = value
    }

    func width(of itemWidths: [Int]) -> Int {
        precondition(itemWidths.allSatisfy { $0 >= 0 })
        let gapCount = max(itemWidths.count - 1, 0)
        return itemWidths.reduce(0, +) + gapCount * gap
    }
}

struct RowLayout {
    let calculator: SpacingCalculator

    init(calculator: SpacingCalculator = .shared) {
        self.calculator = calculator
    }

    func width(of itemWidths: [Int]) async -> Int {
        await calculator.width(of: itemWidths)
    }
}

final class RowLayoutTests: XCTestCase {
    private var calculator: SpacingCalculator?

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

    override func tearDown() {
        calculator = nil
        super.tearDown()
    }

    func testDefaultGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 64)
    }

    func testWideGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        await fixture.setGap(8)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 76)
    }

    func testEmptyRow() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [])

        XCTAssertEqual(actual, 0)
    }

    func testSingleItem() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10])

        XCTAssertEqual(actual, 10)
    }
}

A linha decisiva é calculator = SpacingCalculator(). Cada configuração cria um ator independente com espaço 2. Em seguida, cada teste passa esse ator para RowLayout. Chamar RowLayout() dentro desses testes selecionaria .shared e ignoraria a fixture.

O exemplo permite deliberadamente a construção direta. Se o singleton existente tiver um inicializador privado, introduza um caminho de construção interno ou extraia o estado mutável dos cálculos para uma dependência que possa ser construída separadamente. Um inicializador interno pode ser disponibilizado a um destino de teste por meio do mecanismo estabelecido de importação testável do módulo de aplicativo; não é necessário tornar globalmente substituível a referência ao singleton.

A aritmética também define dois casos extremos úteis: uma linha vazia tem largura 0, e um único item não tem espaço intermediário. O exemplo aceita inteiros não negativos pequenos o bastante para evitar estouro de inteiro; valores negativos violam suas pré-condições e encerram a execução em vez de produzir uma falha de asserção.

Os cálculos esperados e as duas ordens de execução dos testes foram verificados localmente com cálculos determinísticos em Python. Os exemplos em Swift foram inspecionados, mas não compilados nem executados, pois este ambiente de pesquisa não tem uma cadeia de ferramentas Swift. Execute-os no Xcode para verificar a compilação e a execução no seu destino.

Quando redefinir o singleton é suficiente?

Redefinir o singleton é uma solução temporária viável quando os testes realmente precisam da instância compartilhada e todo o estado visível aos testes pode ser restaurado. Estabeleça a base antes de cada teste, inclusive para os valores que um teste pressupõe serem padrões. Redefinir apenas a propriedade alterada pelo teste que falha no momento pode deixar outras dependências sem ser descobertas.

Para esta calculadora, a configuração assíncrona poderia selecionar SpacingCalculator.shared e aguardar setGap(2) antes de cada método. Isso estabelece a base aritmética somente se nenhum chamador simultâneo alterar o espaço antes da asserção. Se a fixture passar a incluir resultados em cache ou outras configurações, a operação que restabelece a base também deverá abranger esses valores.

Um ator não concede a um teste propriedade exclusiva entre várias chamadas aguardadas. Por exemplo, o teste A pode definir o espaço compartilhado como 2, o teste B pode defini-lo como 8, e então o teste A pode calcular uma largura usando 8. Cada operação do ator pode ser executada corretamente, enquanto a suposição geral do teste falha. Isso decorre das regras documentadas de execução e suspensão de atores no Swift.

Antes de escolher o isolamento por redefinição, identifique todos os testes e tarefas em segundo plano que podem acessar o objeto compartilhado. Se não puder garantir o uso exclusivo durante toda a preparação, execução e limpeza, injete estado independente. Restaurar um instantâneo depois não impede que outro chamador observe alterações temporárias durante o teste.

Como a fixture se traduz para o Swift Testing?

O Swift Testing inicializa uma instância distinta da suíte para cada função de teste de instância. Portanto, uma propriedade de instância que constrói uma calculadora oferece o mesmo padrão de propriedade, sem substituições de configuração do XCTest. Uma propriedade inicializada a partir de .shared ainda apontaria para o singleton. A Apple documenta o ciclo de vida de instâncias em Organizar funções de teste com tipos de suíte.

Com as definições anteriores de SpacingCalculator e RowLayout presentes uma única vez no mesmo destino de teste, esta é uma suíte completa adicional do Swift Testing. Ela exige uma cadeia de ferramentas que inclua o módulo Testing, como o Xcode 16 ou posterior. O isolamento de ator padrão também deve ser nonisolated.

import Testing

struct RowLayoutSwiftTests {
    let calculator = SpacingCalculator()

    @Test func defaultGap() async {
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 64)
    }

    @Test func wideGap() async {
        await calculator.setGap(8)
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 76)
    }
}

Para preparações mais complexas, o Swift Testing permite inicializadores de suíte, incluindo inicializadores assíncronos ou que lançam erros. O artigo da Apple Migrar um teste do XCTest explica essa substituição dos métodos de configuração do XCTest. Esta fixture não precisa de limpeza explícita porque possui apenas estado de cálculo local e não inicia trabalho em segundo plano.

Por padrão, o Swift Testing executa os testes em paralelo. Aplicar .serialized a uma suíte executa serialmente os testes contidos nela e as suítes aninhadas, mas não impede que testes não relacionados sejam executados ao mesmo tempo. Aplicar o atributo a uma função de teste comum, sem parâmetros, não tem efeito. Essas limitações estão explícitas em Executar testes em série ou em paralelo.

Consequentemente, colocar duas suítes dependentes do singleton sob anotações .serialized separadas não estabelece exclusão mútua entre elas. Um pai comum serializado pode coordenar seus descendentes, desde que todos os usuários concorrentes pertençam a esse escopo. Fixtures novas evitam essa necessidade de coordenação para estes testes de cálculo.

Verifique os resultados individuais e da suíte no Xcode

Use o mesmo esquema, plano de teste e destino Mac durante toda a verificação. A Apple documenta a execução de métodos individuais, suítes e do plano de teste ativo em Executar testes e interpretar resultados.

  1. Execute cada um dos quatro métodos XCTest individualmente usando o controle de reprodução na margem do editor. Espere 64, 76, 0 e 10, respectivamente.
  2. Execute a classe RowLayoutTests usando o controle de reprodução ao lado de sua declaração. Os quatro métodos devem passar em conjunto.
  3. Escolha Product > Test para executar o plano de teste ativo. Confirme que esses testes foram realmente executados, em vez de ignorados ou excluídos.
  4. Se você adicionou o exemplo do Swift Testing, execute também suas duas funções individualmente e em conjunto.

Se os testes individuais passarem, mas o plano completo falhar, rastreie o objeto usado pelo consumidor que falhou. Procure uma injeção não realizada, um auxiliar que chama .shared ou trabalho assíncrono que continua após o teste que o iniciou. Se um teste falhar isoladamente, verifique suas entradas e o cálculo esperado antes de atribuir a falha à ordem da suíte.

A fixture finalizada deve deixar visível o estado inicial no caminho de construção do teste atual. Para estes cálculos de layout, cada teste possui uma calculadora, fornece-a explicitamente ao consumidor e aguarda seu trabalho. Assim, o resultado esperado independe de qual teste vizinho foi executado primeiro.

Fontes