Тестирование
Что проверяет модульный тест, тестовые двойники, тестирование async- и actor-изолированного кода, фреймворк Swift Testing, снапшоты и о чём умалчивает покрытие.
10 вопросов
MiddleКодОчень частоВыделите протокол из URLSession.shared, чтобы view controller стал тестируемым
Выделите протокол из URLSession.shared, чтобы view controller стал тестируемым
Определите протокол DataFetching с нужным методом, сделайте URLSession соответствующим ему и внедрите DataFetching со значением по умолчанию .shared. Тесты подставляют FakeFetcher с заготовленными данными или ошибкой; сеть не трогается.
Типичные ошибки
- ✗Пытаются наследовать или переопределить
URLSessionвместо протокола-обёртки - ✗Переприсваивают глобальный синглтон
sharedвместо внедрения зависимости - ✗Оставляют контроллер зовущим
URLSession.sharedвнутри после рефакторинга
Уточняющие вопросы
- →Почему инъекция протокола чище, чем наследование от
URLSession? - →Как значение по умолчанию у параметра
initсохраняет прод-код без правок?
MiddleКодОчень частоПротестируйте async throws-функцию и проверьте, что брошена именно нужная ошибка
Протестируйте async throws-функцию и проверьте, что брошена именно нужная ошибка
Дождитесь вызова в async-тесте и проверьте конкретный случай ошибки, а не просто факт броска. Используйте await #expect(throws: LoaderError.notFound) { ... } или XCTest-блок do/catch, который валит тест при успехе и проверяет .notFound.
Типичные ошибки
- ✗Проверяют лишь факт броска ошибки, а не какой именно случай
- ✗Блокируют семафором вместо написания async-теста
- ✗Забывают валить тест, когда вызов неожиданно вернул значение
Уточняющие вопросы
- →Как
#expect(throws:)сравнивает брошенную ошибку с ожидаемым случаем? - →Почему тест обязан упасть, если вызов вернул значение, а не бросил?
JuniorТеорияЧастоЧем отличаются unit-, integration- и UI-тесты по охвату и цене и сколько каждого нужно?
Чем отличаются unit-, integration- и UI-тесты по охвату и цене и сколько каждого нужно?
Unit-тест проверяет один компонент в изоляции — быстро и дёшево. Integration-тест — части вместе (репозиторий и хранилище) — медленнее. UI-тест гоняет всё приложение — медленнее и хрупче всего. Много unit, меньше integration, мало UI.
Типичные ошибки
- ✗Думают, что UI-тесты достаточно дешёвы, чтобы составлять основную массу набора
- ✗Считают integration-тест просто unit-тестом на другом фреймворке
- ✗Верят, что больше UI-тестов всегда означает лучшее покрытие
Уточняющие вопросы
- →Почему UI-тесты самые хрупкие из трёх?
- →Что относится к integration-тесту и не покрывается unit-тестом?
JuniorТеорияЧастоЧто даёт модульный тест и в чём смысл структуры Arrange-Act-Assert?
Что даёт модульный тест и в чём смысл структуры Arrange-Act-Assert?
Модульный тест проверяет одну маленькую часть логики в изоляции — быстрая, повторяемая защита от регрессий. Arrange-Act-Assert задаёт структуру: подготовьте входные данные, вызовите код, затем убедитесь, что результат совпал с ожиданием.
Типичные ошибки
- ✗Считают, что модульный тест обязан трогать реальные зависимости вроде сети или диска
- ✗Путают модульный тест со сквозным UI-тестом, который гоняет всё приложение
- ✗Считают Arrange-Act-Assert необязательным ритуалом, а не структурой для читаемости
Уточняющие вопросы
- →Почему модульному тесту стоит избегать реальной сети или доступа к диску?
- →Чем одна ясная проверка на тест упрощает диагностику падения?
MiddleДизайнЧастоВаш набор показывает 85% покрытия строк, но баги всё равно уходят в прод. Объясните, что на самом деле измеряет покрытие строк, почему высокое число всё ещё упускает реальные дефекты и что бы вы добавили или изменили, чтобы ловить больше. Учтите, что покрытие считает выполненные строки, а не проверенное поведение, что тест может пройти по строке, ничего осмысленного о ней не проверив, и что целые классы дефектов — неверные условия ветвей, необработанные краевые случаи, гонки и стыки интеграции — могут вообще не задеваться. Скажите, для чего покрытие реально полезно, какую цель вы бы задали и какие дополнительные сигналы или виды тестов ввели бы, чтобы поднять реальную уверенность.
Ваш набор показывает 85% покрытия строк, но баги всё равно уходят в прод. Объясните, что на самом деле измеряет покрытие строк, почему высокое число всё ещё упускает реальные дефекты и что бы вы добавили или изменили, чтобы ловить больше. Учтите, что покрытие считает выполненные строки, а не проверенное поведение, что тест может пройти по строке, ничего осмысленного о ней не проверив, и что целые классы дефектов — неверные условия ветвей, необработанные краевые случаи, гонки и стыки интеграции — могут вообще не задеваться. Скажите, для чего покрытие реально полезно, какую цель вы бы задали и какие дополнительные сигналы или виды тестов ввели бы, чтобы поднять реальную уверенность.
Покрытие строк считает, какие строки выполнились, а не проверила ли проверка их поведение, поэтому тест может пройти по строке и не проверить ничего. Высокое число упускает неверные ветви, краевые случаи и гонки. Это поиск пробелов, а не цель — добавьте покрытие ветвей и mutation-тесты.
Типичные ошибки
- ✗Принимают процент покрытия за меру реально проверенного поведения
- ✗Верят, что 100% покрытия строк означает полностью протестированный код
- ✗Добавляют тесты, лишь проходящие по строкам, со слабыми проверками ради числа
Уточняющие вопросы
- →Как покрытие ветвей ловит дефекты, которые упускает покрытие строк?
- →Что показывает mutation-тестирование, чего не может число покрытия?
MiddleТеорияЧастоТестовые двойники — чем отличаются dummy, stub, spy, mock и fake и какой нужен чаще всего?
Тестовые двойники — чем отличаются dummy, stub, spy, mock и fake и какой нужен чаще всего?
Dummy заполняет место и не используется. Stub возвращает заготовленные значения. Spy — это stub, который ещё записывает вызовы. Mock несёт ожидания и падает, если не выполнены. Fake — лёгкая рабочая реализация, напр. хранилище в памяти. Чаще всего берут stub (и fake).
Типичные ошибки
- ✗Называют "mock" любой тестовый двойник без разбора
- ✗Путают spy, который записывает вызовы, с mock, который проверяет ожидания
- ✗Берут тяжёлый mock там, где хватило бы простого stub
Уточняющие вопросы
- →Когда встроенное ожидание mock делает тест хрупким?
- →Почему для репозитория fake в памяти часто лучше stub?
MiddleКодИногдаПротестируйте зависящий от времени код (debounce) без Thread.sleep, внедрив clock
Протестируйте зависящий от времени код (debounce) без Thread.sleep, внедрив clock
Сделайте время зависимостью, а не глобальным вызовом. Внедрите абстракцию — протокол Scheduler или Clock — и планируйте через неё, по умолчанию беря реальную в проде. Тест передаёт виртуальный планировщик и прокручивает его без Thread.sleep.
Типичные ошибки
- ✗Ждут реальное время через
Thread.sleepвместо виртуального - ✗Прикрывают недетерминированное время таймаутом ожидания
- ✗Зовут таймеры
DispatchQueueнапрямую, а не через внедрённый планировщик
Уточняющие вопросы
- →Как виртуальный планировщик прокручивает время без реального ожидания?
- →Почему обнуление задержки меняет поведение, которое вы хотели проверить?
MiddleТеорияИногдаЧем @Test-фреймворк Swift Testing от Apple отличается от XCTest и могут ли оба сосуществовать в одной цели?
Чем @Test-фреймворк Swift Testing от Apple отличается от XCTest и могут ли оба сосуществовать в одной цели?
Swift Testing использует @Test с макросами #expect/#require, параметризованные тесты и трейты, параллельно; XCTest — на XCTestCase и XCTAssert. #require прерывает, #expect продолжает. Оба сосуществуют в одной цели; UI-тесты — на XCUITest.
Типичные ошибки
- ✗Считают, что Swift Testing заменяет и убирает XCTest, а не сосуществует с ним
- ✗Думают, что
#expectпрерывает тест так же, как#require - ✗Забывают, что UI-тесты по-прежнему опираются на XCUITest, а не Swift Testing
Уточняющие вопросы
- →Когда вы возьмёте
#requireвместо#expect? - →Как параметризованные
@Testсокращают дублирование в тестах?
MiddleДизайнИногдаВы ведёте тестирование экрана ленты и должны решить, что покрыть unit-тестами, что снапшот-тестами, а что UI-тестами, не перевернув пирамиду тестов. В ленте есть view model, форматирующая метки времени и делающая пагинацию, кастомные ячейки с несколькими состояниями раскладки (загрузка, ошибка, загружено, пусто) и переход по тапу с ячейки на экран деталей. Скажите, какой слой владеет каждой заботой, почему снапшот-тесты подходят раскладке ячеек, почему сквозных UI-тестов должно быть мало и какие признаки говорят, что пирамида переворачивается в сторону слишком многих медленных, хрупких тестов.
Вы ведёте тестирование экрана ленты и должны решить, что покрыть unit-тестами, что снапшот-тестами, а что UI-тестами, не перевернув пирамиду тестов. В ленте есть view model, форматирующая метки времени и делающая пагинацию, кастомные ячейки с несколькими состояниями раскладки (загрузка, ошибка, загружено, пусто) и переход по тапу с ячейки на экран деталей. Скажите, какой слой владеет каждой заботой, почему снапшот-тесты подходят раскладке ячеек, почему сквозных UI-тестов должно быть мало и какие признаки говорят, что пирамида переворачивается в сторону слишком многих медленных, хрупких тестов.
Unit-тестами покройте чистую логику view model — форматирование, пагинацию, переходы. Снапшот-тестами — состояния раскладки ячейки, дёшево ловя визуальные регрессии. UI-тесты держите лишь для пары сценариев тапа на детали. Рост медленных хрупких UI-тестов — знак переворота пирамиды.
Типичные ошибки
- ✗Гонят сквозные UI-тесты покрывать логику, которой владеет unit-тест
- ✗Снапшотят бизнес-логику вместо визуальных состояний раскладки
- ✗Читают высокое число UI-тестов как тщательность, а не как тревожный знак
Уточняющие вопросы
- →Какие состояния ячейки стоят снапшота, а какие лишь добавляют шум?
- →Что делает UI-тесты медленнее и более хрупкими, чем unit-тесты?
MiddleТеорияИногдаТесты с колбэками нестабильны — на что ждёт XCTestExpectation из XCTest и как их стабилизировать?
Тесты с колбэками нестабильны — на что ждёт XCTestExpectation из XCTest и как их стабилизировать?
XCTestExpectation выполняют в колбэке; wait(for:timeout:) ждёт его или таймаута. Нестабильность — от короткого таймаута, пропущенного/двойного fulfill() или гонки. Помогут реалистичный таймаут, один fulfill() на ветку и внедрённый планировщик.
Типичные ошибки
- ✗Лечат нестабильность раздуванием таймаута вместо устранения гонки
- ✗Забывают вызвать
fulfill()или зовут его больше одного раза - ✗Ставят
Thread.sleepвместо ожидания, чтобы дождаться колбэка
Уточняющие вопросы
- →Почему раздутый таймаут прячет, а не чинит гонку?
- →Как внедрённый планировщик делает асинхронное время детерминированным?