При переносе модульных тестов 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
Диагностика слабой ссылки, проверка которой изменилась после переноса
Если перенесённый тест владения завершается с ошибкой, проверяйте следующее по порядку:
- Проверьте момент наблюдения. Предполагаемое освобождение должно произойти до проверки слабой ссылки.
- Проверьте свойства фикстуры. Другое свойство может продолжать владеть калькулятором после очистки одного из свойств.
- Проверьте локальные псевдонимы и захваты замыканий. Распакованный объект или сильно захваченная ссылка могут продлить владение до завершения проверки.
- Проверьте область видимости вспомогательной функции. Отложенная очистка вспомогательной функции может выполниться раньше, чем того требует контракт теста.
- Проверьте незавершённую работу. Если реальный объект запускает асинхронную работу, определите и дождитесь её завершения, прежде чем применять ожидание синхронного освобождения.
Ненулевое значение слабой ссылки nil указывает на то, что объект существует в момент проверки. Прежде чем менять утверждение или добавлять задержку, выясните, кто ещё владеет объектом. В этом искусственном примере нет асинхронной работы, выполнения которой нужно дожидаться, а диагностический контроль точно показывает, как дополнительный владелец влияет на результат.
Перенос завершён, когда вычисление и проверки владения выражают тот же предполагаемый результат, что и исходный тест: вычислить 12, удалить предполагаемое владение фикстуры и проверить освобождение через независимо хранимую слабую ссылку. Сохраняйте эту последовательность явной при дальнейших изменениях теста.
Источники
Настройка и очистка состояния в тестах — документация Apple для разработчиков.
Перенос теста с XCTest — проект Swift.
Деинициализация — проект Swift, язык программирования Swift.
Автоматический подсчёт ссылок — проект Swift, язык программирования Swift.
Добавление тестов в проект Xcode — документация Apple для разработчиков.
Поток управления — проект Swift, язык программирования Swift.
Ожидания и подтверждения — проект Swift.
Обработка ошибок — проект Swift, язык программирования Swift.
Организация тестовых функций с помощью типов наборов тестов — проект Swift.
TestScoping — документация Apple для разработчиков.