Зачем нужны тесты
Когда функция небольшая, вы обычно и так держите в голове, что она должна делать, и проверяете её вручную: запустили программу, посмотрели на вывод — вроде бы всё верно. Но стоит функции обрасти условиями и особыми случаями — пустая строка, отрицательное число, пустая коллекция — как ручная проверка перестаёт быть надёжной. Вы либо забываете какой-то случай, либо, изменив код через месяц, случайно ломаете то, что раньше работало, и узнаёте об этом от пользователей, а не от себя.
Тест — это код, который проверяет другой код: вызывает функцию с конкретными входными данными и сравнивает результат с тем, что ожидалось. Написали тест один раз — можете запускать его сколько угодно раз при каждом изменении, и он честно скажет, не сломалось ли что-то.
Официальные термины: Swift Testing
В экосистеме Swift есть современный фреймворк для тестирования — Swift Testing. Он подключается через import Testing и постепенно вытесняет более старый XCTest в новых проектах. Основные элементы:
@Test— атрибут перед функцией, который превращает обычную функцию в тест. В отличие отXCTest, тестовой функции не обязательно называться с приставкиtestи не обязательно быть методом класса, унаследованного от специального базового типа.#expect(...)— макрос, который проверяет булево выражение. Если оно ложно, тест помечается как проваленный, но выполнение функции продолжается дальше — за один запуск вы узнаёте обо всех проблемах, а не только о первой.#require(...)— похож на#expect, но при провале сразу прерывает тест. Используется, когда без выполнения условия проверять дальше бессмысленно — например, когда нужно развернуть опционал перед следующими шагами.
Как выглядит тест
import Testing
@Test func сложениеПоложительных() {
let result = 2 + 2
#expect(result == 4)
}
Функция помечена @Test, поэтому среда тестирования находит её сама и запускает без дополнительной регистрации. Внутри #expect проверяет обычное булево выражение: если оно ложно, тест зафиксирует провал вместе с местом в коде, где это произошло.
@Test func первыйЭлементСуществует() throws {
let items = [1, 2, 3]
let first = try #require(items.first)
#expect(first == 1)
}
Здесь #require разворачивает опционал items.first. Если бы массив оказался пустым, #require сразу прервал бы тест — не пришлось бы дальше работать со значением nil и получать неочевидную ошибку где-то ниже по коду.
Параметризованные тесты
Если одна и та же проверка должна пройти для нескольких входных значений, не обязательно копировать функцию — можно передать список аргументов прямо в атрибут:
@Test(arguments: [1, 4, 9, 16])
func этоТочныйКвадрат(_ n: Int) {
let root = Int(Double(n).squareRoot())
#expect(root * root == n)
}
Тест запустится четыре раза — по одному на каждое значение из массива, и в отчёте будет видно, для какого именно аргумента проверка не прошла. Это избавляет от четырёх почти одинаковых функций с разными числами внутри.
Что стоит проверять тестами
Тестами имеет смысл покрывать код с логикой: функции, которые вычисляют, преобразуют, валидируют — то есть принимают вход и по чётким правилам дают выход. Хорошие кандидаты — разбор строки, проверка условия, подсчёт чего-либо, сортировка по своему правилу.
Не стоит писать тесты ради самого факта их наличия: проверка того, что 1 + 1 == 2, или тест, который просто вызывает функцию и ничего не сравнивает, не приносит пользы. Также обычно не тестируют код, зависящий от внешнего мира без специальной подготовки — сеть, текущее время, случайные числа: результат каждый раз разный, и тест будет то падать, то проходить без изменений в коде.
Чем это отличается от XCTest
XCTest требует, чтобы тестовый метод жил внутри класса-наследника XCTestCase и назывался с приставки test — например, testДеление(). Проверки там делаются через семейство функций XCTAssert... (XCTAssertEqual, XCTAssertTrue и другие). Swift Testing устроен свободнее: тест — это обычная функция с атрибутом @Test, её можно объявить где угодно в тестовом модуле, а параметризация из коробки избавляет от ручного дублирования. Оба подхода до сих пор используются в реальных проектах, но новые проекты чаще выбирают Swift Testing.
От теории к практике
В редакторе задач этого курса пакет Testing недоступен, поэтому запустить @Test и #expect прямо здесь не получится. Зато можно потренироваться в том, из чего тестирование складывается по сути: написать функцию, которую легко проверить, и написать саму проверку — обычным кодом, без специального фреймворка. Именно этим вы и займётесь в задаче ниже.
Частые ошибки
- Писать функцию так, что её результат зависит от внешнего состояния — глобальной переменной, файла, времени. Такую функцию тяжело тестировать: при одинаковом входе она может давать разный результат.
- Проверять сразу много всего в одном тесте так, что при провале непонятно, что именно сломалось. Лучше несколько маленьких проверок с понятными именами, чем одна большая.
- Путать
#expectи#require: если после проверки код обращается к развёрнутому значению, а вы использовали#expect, то при провале программа всё равно попытается использовать несуществующее значение и упадёт с посторонней ошибкой. - Тестировать только «счастливый путь» и забывать про пустую строку, пустую коллекцию, нуль, отрицательное число — именно на этих случаях чаще всего находятся настоящие ошибки.
Резюме
- Тест — это код, который вызывает вашу функцию и сравнивает результат с ожидаемым, чтобы не проверять всё вручную заново при каждом изменении.
Swift Testingподключается черезimport Testing; тестовая функция помечается атрибутом@Test.#expectфиксирует провал и продолжает тест;#requireпри провале сразу прерывает выполнение.@Test(arguments: [...])запускает один тест несколько раз с разными значениями без копирования кода.- Тестами стоит покрывать функции с чёткой логикой и обязательно проверять граничные случаи, а не только обычный сценарий.
Проверьте себя
3 вопроса
Функция и её собственная проверка
В песочнице этого курса нет пакета Testing, поэтому напрямую воспользоваться @Test и #expect не получится. Вместо этого напишите две обычные функции — так, как если бы вы готовили код к тестированию вручную.
Первая функция — isPalindrome(_ text: String) -> Bool. Она должна определять, читается ли строка одинаково слева направо и справа налево, без учёта регистра букв. Например, "шалаш" и "Kayak" — палиндромы, а "Swift" — нет. Пустая строка и строка из одного символа считаются палиндромами.
Вторая функция — checkResult(_ actual: Bool, expected: Bool) -> Bool. Она играет роль простейшей проверки в духе #expect: должна вернуть true, если actual совпадает с expected, и false в противном случае.