При переносе модульных тестов XCTest на Mac сохраняйте последовательность управления владением: завершите работу, освободите нужные сильные ссылки, затем проверьте слабую ссылку. Для синхронной очистки локальный для теста defer явно задаёт эту последовательность.

В этом руководстве рассматривается перенос теста с проверкой владения в Xcode. Пример вычисляет количество ячеек в искусственной сетке, а затем проверяет, что калькулятор освобождён. В нём используются только локальные объекты Swift и целочисленная арифметика.

Какое поведение жизненного цикла необходимо сохранить при переносе?

XCTest предоставляет последовательность, управляемую фреймворком. После завершения метода теста он выполняет зарегистрированные блоки очистки в обратном порядке регистрации, а затем методы очистки экземпляра. Зарегистрированная очистка выполняется и при ошибке утверждения в тесте; завершение процесса может помешать её выполнению. Эти детали важны, когда один блок освобождает объект, а другой проверяет его слабую ссылку. Настройка и очистка состояния в тестах

Например, блок очистки, который проверяет наличие nil, должен выполниться после блока, удаляющего последнего предполагаемого владельца. Поскольку XCTest использует обратный порядок регистрации, проверка регистрируется первой, а освобождение — вторым. Если поменять порядок регистрации, тест будет наблюдать другое состояние.

Руководство по переносу на Swift Testing предлагает заменить настройку инициализатором, а очистку — деинициализатором класса или акторного набора тестов. Такое соответствие полезно для очистки, связанной с жизненным циклом набора. Для проверки владения нужно принять дополнительное решение: какую именно ссылку следует удалить до выполнения проверки? Перенос теста с XCTest

Перед изменением синтаксиса теста воспользуйтесь этой таблицей решений:

Текущая задача Вариант переноса Причина
Создать новое состояние фикстуры для каждого теста экземпляра Инициализация сохранённого свойства или init() Подготавливает фикстуру до запуска теста
Освободить объект и сразу проверить слабую ссылку Явное освобождение с последующим #expect, обычно внутри локального defer Делает момент проверки явным
Выполнить синхронную очистку, связанную с жизненным циклом набора тестов ссылочного типа Набор тестов класса или актора с deinit Следует документированному соответствию жизненного цикла
Дождаться очистки или сообщить об ошибке при очистке Ожидаемая очистка в асинхронной функции или подходящая пользовательская область видимости Нужен путь выполнения, поддерживающий приостановку и обработку ошибок

Таблица помогает принять решение при переносе на основе документированного жизненного цикла и правил языка. Она не подразумевает, что каждому хуку XCTest соответствует взаимозаменяемый метод Swift Testing.

Почему перенос проверки слабой ссылки в deinit может изменить результат теста?

Swift вызывает деинициализатор класса непосредственно перед освобождением экземпляра. Деинициализатор ещё может обращаться к сохранённым свойствам этого экземпляра. Следовательно, сам факт входа в deinit набора тестов не доказывает, что объект, удерживаемый одним из этих свойств, уже освобождён. Деинициализация

Рассмотрим набор тестов с сильным свойством layout. Слабая ссылка на этот макет может оставаться не-nil во время выполнения деинициализатора набора. Прежде чем выяснять, освобождён ли макет, тест должен удалить сильную ссылку на него из свойства.

Есть и другой сценарий сбоя: цикл ссылок, удерживающий набор тестов, может помешать вызову его деинициализатора. Тогда утверждение, помещённое только в этот деинициализатор, не сможет сообщить об удержании. Это следует из правил владения ARC: сильные ссылки сохраняют экземпляры, а слабые не мешают освобождению и становятся nil после освобождения экземпляра, на который ссылаются. Автоматический подсчёт ссылок

В тесте владения храните наблюдателя вне объекта, освобождение которого проверяется. Предусмотрите отдельную операцию освобождения и выполняйте проверку в обычном потоке выполнения теста. Так контракт теста можно проверить, не полагаясь на освобождение самого набора тестов.

Подготовьте исходный тест XCTest в Xcode

Используйте существующую цель модульных тестов в Xcode 16 или более поздней версии. Apple указывает, что XCTest и Swift Testing могут сосуществовать в одной цели и пакете, поэтому для переноса не нужен отдельный пакет модульного тестирования. Чтобы добавить файл теста, выберите File > New > New File From Template, затем XCTest Unit Test или Swift Testing Unit Test в зависимости от ситуации. Добавление тестов в проект Xcode

Ниже приведён полный файл теста XCTest. Его калькулятор возвращает 3 * 4, поэтому ожидаемый результат — 12. Два блока очистки намеренно показывают требуемый порядок выполнения.

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
        )
    }
}

Блок освобождения удерживает тестовый экземпляр, чтобы обновить свойство фикстуры. Блок проверки захватывает калькулятор слабо, избегая дополнительного сильного владельца в самой проверке. Apple указывает, что блоки очистки предназначены для специфической очистки теста и регистрируются рядом с соответствующей настройкой. Настройка и очистка состояния в тестах

Контракт исходного теста узко определён: после завершения тела теста и обнуления свойства фикстуры калькулятор больше не должен существовать. Тест не измеряет потребление памяти и не устанавливает, что освобождён каждый объект, созданный приложением.

Перенесите последовательность владения в локальную область видимости

Для этого синхронного случая используйте набор тестов-структуру и поместите освобождение и проверку в один блок defer. Swift выполняет отложенный код при выходе из охватывающей его области видимости, в том числе при раннем возврате или выброшенной ошибке. Несколько блоков отложенного выполнения запускаются в обратном порядке; один блок позволяет нагляднее показать в этом примере, что освобождение происходит до проверки. Поток управления

Ниже приведён полный самостоятельный файл Swift Testing. Замените им исходный файл или храните примеры в отдельных файлах, чтобы их закрытые вспомогательные типы оставались раздельными. Второй тест — диагностический контроль: он показывает, что дополнительный сильный владелец поддерживает слабую ссылку до тех пор, пока этот владелец не будет обнулён.

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)
    }
}

В первом тесте фикстура владеет калькулятором, а observed наблюдает за ним, не владея им. Вычисление возвращает необязательное целое число, которое #require извлекает из опционального значения. Распакованный калькулятор не сохраняется в другой локальной переменной. При выходе из области видимости отложенный блок очищает ссылку фикстуры и проверяет наблюдателя.

Ошибка #expect регистрирует проблему и позволяет выполнению продолжиться. Ошибка #require выбрасывает исключение, останавливая этот путь выполнения. Поскольку defer регистрируется до проверки требования, очистка и проверка слабой ссылки всё равно выполняются, если требование выбрасывает исключение. Ожидания и подтверждения, Обработка ошибок

Диагностический контроль показывает два отдельных ожидаемых состояния: после очистки одного из двух владельцев наблюдатель остаётся не-nil; после очистки последнего владельца он становится nil. Это наглядные проверки владения, а не результаты измерений приложения. Примеры на Swift были изучены, но скомпилировать их в исследовательской среде не удалось из-за отсутствия исполняемого файла Swift.

Проверьте границы вспомогательных функций, конкурентность и асинхронную очистку

Локальный defer относится к охватывающей его области видимости. Если перенести отложенный блок во вспомогательную функцию-фабрику, он выполнится при возврате из неё — возможно, до того, как тест воспользуется возвращённым объектом. Оставьте блок в тесте или спроектируйте вспомогательную функцию так, чтобы вся операция выполнялась в её области видимости. Охватывающая область является частью контракта владения. Поток управления

Также проверьте, где находится фикстура. Swift Testing создаёт отдельный экземпляр набора для каждого метода теста экземпляра, а тесты внутри набора по умолчанию выполняются параллельно. Локальное для экземпляра или теста состояние соответствует этой модели; глобальная фикстура или статическая коллекция создаёт владение, общее для нескольких тестов. Организация тестовых функций с помощью типов наборов тестов

Если коду приложения для Mac требуется главный актор, явно изолируйте тест Swift Testing с помощью @MainActor. В руководстве по переносу отмечено, что для синхронных методов тестов Swift Testing не наследует поведение XCTest с главным актором по умолчанию. Для искусственного калькулятора выше такое требование отсутствует. Перенос теста с XCTest

Синхронный блок отложенного выполнения не заменяет ожидаемую очистку. Отложенные действия Swift не могут выбросить ошибку за пределы блока. Если очистка может завершиться с ошибкой, явно обработайте и сообщите о ней, а не игнорируйте её. Оставьте ожидаемую очистку в асинхронном пути выполнения и проследите, чтобы до неё доходили как успешные, так и завершающиеся ошибкой пути выполнения тела. Обработка ошибок

Для повторно используемой настройки и очистки, управляемых фреймворком, в современной Swift Testing предусмотрен TestScoping, который задаёт пользовательское выполнение вокруг теста или набора. Apple указывает доступность начиная со Swift 6.1 и Xcode 16.3. Это поддерживаемый подход, если нескольким тестам нужна общая область выполнения, но для него требуются пользовательский trait и поставщик области видимости; простого переименования addTeardownBlock недостаточно. TestScoping

Диагностика слабой ссылки, проверка которой изменилась после переноса

Если перенесённый тест владения завершается с ошибкой, проверяйте следующее по порядку:

  1. Проверьте момент наблюдения. Предполагаемое освобождение должно произойти до проверки слабой ссылки.
  2. Проверьте свойства фикстуры. Другое свойство может продолжать владеть калькулятором после очистки одного из свойств.
  3. Проверьте локальные псевдонимы и захваты замыканий. Распакованный объект или сильно захваченная ссылка могут продлить владение до завершения проверки.
  4. Проверьте область видимости вспомогательной функции. Отложенная очистка вспомогательной функции может выполниться раньше, чем того требует контракт теста.
  5. Проверьте незавершённую работу. Если реальный объект запускает асинхронную работу, определите и дождитесь её завершения, прежде чем применять ожидание синхронного освобождения.

Ненулевое значение слабой ссылки nil указывает на то, что объект существует в момент проверки. Прежде чем менять утверждение или добавлять задержку, выясните, кто ещё владеет объектом. В этом искусственном примере нет асинхронной работы, выполнения которой нужно дожидаться, а диагностический контроль точно показывает, как дополнительный владелец влияет на результат.

Перенос завершён, когда вычисление и проверки владения выражают тот же предполагаемый результат, что и исходный тест: вычислить 12, удалить предполагаемое владение фикстуры и проверить освобождение через независимо хранимую слабую ссылку. Сохраняйте эту последовательность явной при дальнейших изменениях теста.

Источники