Как проверить уменьшение миниатюр iOS на облачном Mac

Как проверить уменьшение миниатюр iOS на облачном Mac

Фотография высокого разрешения занимает совсем небольшую ячейку в списке iOS, но при быстрой прокрутке может декодироваться снова и снова. Если код сначала загружает исходное изображение, а затем уменьшает UIImage до размера отображения, на экране будет видна миниатюра, хотя при декодировании уже могло быть обработано изображение целиком. При запуске регрессионных тестов на облачном Mac надёжнее сначала превратить требование «максимальная сторона миниатюры не должна превышать заданное число пикселей» в проверяемый контракт, чем просто смотреть, плавно ли прокручивается список.

Сначала задайте предел в пикселях, а не оценивайте размер файла

В этой статье для примера используется предел в 320 пикселей по максимальной стороне. Это условие теста, а не универсальное значение для любого интерфейса. В реальном проекте запрашиваемый размер нужно выводить из размеров ячейки в пунктах, масштаба экрана и способа обрезки изображения. Например, для экрана с высокой плотностью пикселей нельзя рассчитывать выходной размер в пикселях только по размерам ячейки в пунктах. Определив предел, передавайте его в точку загрузки изображений, а не задавайте отдельные константы в каждом контроллере списка.

Добавьте тестовый JPEG в ресурсы тестовой цели под именем landscape.jpg. И ширина, и высота исходного изображения должны превышать тестовый предел: иначе тест может ошибочно пройти, даже если реализация вернёт оригинал без изменений. Храните образец в репозитории, не скачивайте его из сети во время теста. Добавьте также образцы с другими соотношениями сторон, чтобы проверка не ограничивалась одним вариантом компоновки. Проверьте настройки ресурсов тестовой цели и убедитесь, что файл действительно доступен во время выполнения из тестового bundle.

Проверка выходных размеров отвечает на вопрос «какого размера получилась картинка», но не показывает пиковое потребление памяти. Эти два свойства нужно проверять отдельно.

Создавайте миниатюру при декодировании

Создайте источник изображения в ImageIO из URL файла и задайте максимальную сторону в пикселях при создании миниатюры. Пример функции возвращает CGImage, чтобы тест мог напрямую прочитать ширину и высоту в пикселях. Если интерфейсу нужен UIImage, преобразуйте результат в месте вызова. При создании источника отключите немедленное кэширование, чтобы не инициировать декодирование оригинала заранее; при создании миниатюры, напротив, запросите её немедленное декодирование.

import Foundation
import ImageIO

func makeThumbnail(at url: URL, maxPixelSize: Int) -> CGImage? {
    guard maxPixelSize > 0 else { return nil }

    let sourceOptions: [CFString: Any] = [
        kCGImageSourceShouldCache: false
    ]
    guard let source = CGImageSourceCreateWithURL(
        url as CFURL, sourceOptions as CFDictionary
    ) else {
        return nil
    }

    let thumbnailOptions: [CFString: Any] = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
        kCGImageSourceShouldCacheImmediately: true
    ]
    return CGImageSourceCreateThumbnailAtIndex(
        source, 0, thumbnailOptions as CFDictionary
    )
}

Параметр CreateThumbnailFromImageAlways обеспечивает прохождение именно пути создания миниатюры в тесте; CreateThumbnailWithTransform учитывает данные об ориентации изображения. У образцов с разной ориентацией ширина и высота могут поменяться местами, поэтому проверяйте максимальную из двух сторон. Не задавайте для всех изображений правило «ширина должна быть больше высоты». При недопустимом входном значении или невозможности декодировать файл функция возвращает nil. Интерфейсу также нужно предусмотреть видимое состояние ошибки: нельзя считать, что каждый файл корректен.

Проверяйте размеры на фиксированном образце

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

import XCTest
import ImageIO
@testable import Sample

final class ThumbnailTests: XCTestCase {
    func testThumbnailRespectsPixelLimit() throws {
        let url = try XCTUnwrap(
            Bundle(for: Self.self).url(
                forResource: "landscape",
                withExtension: "jpg"
            )
        )
        let image = try XCTUnwrap(
            makeThumbnail(at: url, maxPixelSize: 320)
        )

        XCTAssertGreaterThan(image.width, 0)
        XCTAssertGreaterThan(image.height, 0)
        XCTAssertLessThanOrEqual(
            max(image.width, image.height), 320
        )
    }

    func testRejectsInvalidLimit() throws {
        let url = try XCTUnwrap(
            Bundle(for: Self.self).url(
                forResource: "landscape",
                withExtension: "jpg"
            )
        )
        XCTAssertNil(makeThumbnail(at: url, maxPixelSize: 0))
    }
}

Замените Sample фактическим именем модуля. Помимо проверки размеров, добавьте повреждённый файл и убедитесь, что вызывающий код обрабатывает nil. Добавьте образец с данными об ориентации и проверьте, что изображение отображается как ожидается. У тестовых изображений должны быть понятное происхождение и разрешение на использование: не отправляйте в репозиторий файлы из личной фототеки.

Запускайте тест на облачном Mac и разбирайте ложные срабатывания

Сначала выполните xcrun simctl list devices available, чтобы увидеть установленные симуляторы, затем выберите существующее устройство для параметра destination команды xcodebuild. Имя проекта, scheme и тестовую цель также замените на значения из репозитория. Не копируйте параметры примера, не проверив доступность устройства.

xcrun simctl list devices available
xcodebuild \
  -project Sample.xcodeproj \
  -scheme Sample \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:SampleTests/ThumbnailTests \
  test

Если тест сообщает, что изображение не найдено, сначала проверьте его принадлежность к ресурсам тестовой цели, а не увеличивайте предел размера. Если результат превышает предел, убедитесь, что список действительно вызывает makeThumbnail, а не возвращается к старому пути, где сначала создаётся исходный UIImage. В CI сохраняйте вывод команды для упавших тестов и используйте один и тот же набор файлов-образцов. Если менять изображения, не проверяя их размеры в пикселях и ориентацию, результаты двух запусков перестанут быть сопоставимыми.

Проверяйте размеры и потребление памяти отдельно

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

Напоследок проверьте три вещи: выводится ли запрашиваемый размер миниатюры из требований к отображению; проходят ли все пути загрузки изображений через одну функцию уменьшения при декодировании; предусмотрена ли видимая обработка некорректных изображений. Тогда автоматизированный тест на облачном Mac будет контролировать ограничение в пикселях, а измерения на устройстве — отвечать на вопрос о реальном потреблении памяти. У каждого вида проверки будет своя чёткая область применения.

Часто задаваемые вопросы

Достаточно ли уменьшить уже созданный UIImage?

Нет. Если исходник сначала декодирован целиком, пик потребления памяти уже мог возникнуть. Ограничение нужно задавать при создании миниатюры через ImageIO.

Доказывает ли проверка размеров приемлемый расход памяти?

Нет. XCTest проверяет границу пикселей выходного изображения; пик памяти следует измерять отдельно на целевом устройстве и реальном сценарии прокрутки.

Работайте на выделенных физических узлах

Настройте облачный Mac для следующей задачи

Выберите Mac mini по объёму памяти или нагрузке сборки. Доступны три конфигурации и аренда на день, неделю, месяц или квартал. Актуальный статус узла отображается в панели управления.

Арендовать облачный Mac