Как проверить сортировку списков iOS на облачном Mac
В списке файлов есть «Отчёт2» и «Отчёт10», а после обновления два элемента с одинаковыми именами снова меняются местами. Причина может быть не в обновлении интерфейса, а в компараторе, который не задаёт полный порядок. При запуске тестов iOS на облачном Mac проще воспроизвести проблему, если вынести сортировку из представления и сначала проверить правила на небольшой выборке, а не гадать по записи экрана.
Сначала определите контракт сортировки
В этой статье используются следующие правила для списка файлов: имена сортируются по возрастанию, числа сравниваются в естественном порядке, регистр и диакритические знаки игнорируются. Если имена равны по правилам сравнения, элементы сортируются по возрастанию постоянного бизнес-идентификатора. Локаль сравнения фиксируется, чтобы тесты не зависели от настроек машины.
Это не универсальный вариант для любого продукта. В пользовательской адресной книге может потребоваться сортировка с учётом текущего языка, а в списке файлов, общем для нескольких устройств, — единый порядок на всех устройствах. Сначала определите требования, затем пишите реализацию. Критерий «выглядит нормально» для приёмки не подходит.
Стабильный идентификатор должен оставаться неизменным до и после обновления. Индекс массива, время получения данных и новый UUID, создаваемый при каждом обновлении, не подходят для разрешения равенства имён. На входе также нужно отклонять записи с повторяющимися идентификаторами, иначе даже последний ключ сортировки не позволит различить элементы.
Стабильная сортировка лишь сохраняет относительный порядок равных элементов во входных данных. Она не решает проблему, если сервер каждый раз возвращает их в другом порядке.
Вынесите компаратор в чистую функцию
Добавьте следующий файл в таргет приложения. В примере явно используется en_US_POSIX: это означает, что здесь выбрана фиксированная стратегия сравнения, а не то, что такая локаль подходит для отображения имён на всех языках.
import Foundation
struct ListEntry {
let id: String
let name: String
}
func entryLess(_ lhs: ListEntry, _ rhs: ListEntry) -> Bool {
let order = lhs.name.compare(
rhs.name,
options: [.numeric, .caseInsensitive, .diacriticInsensitive],
range: nil,
locale: Locale(identifier: "en_US_POSIX")
)
if order != .orderedSame {
return order == .orderedAscending
}
return lhs.id < rhs.id
}
Во время сравнения нельзя читать состояние сети, изменять счётчики или запрашивать пользовательские настройки, которые могут меняться. За одну сортировку одни и те же объекты сравниваются многократно, и результат должен быть согласованным. Не используйте для компаратора условие «меньше или равно»: объект не может предшествовать самому себе.
Равенство имён при сравнении не означает, что бизнес-объекты одинаковы. При выбранной стратегии «Resume» и «résumé» могут попасть в одну группу, но это не повод объединять записи. Сортировка, поиск и устранение дубликатов — три разных контракта.
Закрепите ключевое поведение на небольшой выборке
Импортируйте модуль приложения в файл модульных тестов, затем добавьте тестовый класс. Код должен находиться в тестовом таргете, имеющем доступ к типам из примера выше. Дополните выборку в ListSortingTests собственными именами файлов.
import XCTest
final class ListSortingTests: XCTestCase {
let items = [
ListEntry(id: "d", name: "Report10"),
ListEntry(id: "b", name: "report2"),
ListEntry(id: "a", name: "REPORT2"),
ListEntry(id: "c", name: "Report1")
]
func testNaturalOrderAndTies() {
XCTAssertEqual(
items.sorted(by: entryLess).map(\.id),
["c", "a", "b", "d"]
)
}
func testComparatorProperties() {
for a in items {
XCTAssertFalse(entryLess(a, a))
for b in items {
if entryLess(a, b) {
XCTAssertFalse(entryLess(b, a))
}
for c in items {
if entryLess(a, b) && entryLess(b, c) {
XCTAssertTrue(entryLess(a, c))
}
}
}
}
}
func testInputOrderDoesNotChangeOutput() {
let expected = items.sorted(by: entryLess).map(\.id)
XCTAssertEqual(
Array(items.reversed()).sorted(by: entryLess).map(\.id),
expected
)
let rotated = Array(items.dropFirst()) + [items[0]]
XCTAssertEqual(
rotated.sorted(by: entryLess).map(\.id),
expected
)
}
}
Первый тест проверяет порядок чисел и обработку элементов с равными именами. Второй проверяет иррефлексивность, асимметричность и транзитивность, выявляя противоречия, которые возникают при поспешном объединении нескольких правил. Третий меняет входной порядок и проверяет, что результат не зависит от порядка получения данных.
Тройной цикл подходит для десятка с небольшим репрезентативных примеров, но не для полного рабочего списка. Для больших наборов данных отдельно проверяйте время сортировки: проверка свойств не заменяет измерение производительности.
Добавьте граничные случаи Unicode и отсутствующих значений
Дополните набор пустыми именами, ведущими нулями, китайскими именами, комбинируемыми диакритическими знаками и последовательностями эмодзи. Предварительно составленный символ и сочетание буквы с комбинируемым диакритическим знаком могут выглядеть одинаково. Определять их порядок отображения по длине строки нельзя.
Для «文件02» и «文件2» не следует заранее предполагать результат числового сравнения. Сначала решите, должен ли продукт считать их одной группой, затем проверьте поведение в выбранной среде выполнения. Если ведущие нули должны различаться, при равенстве имён можно добавить явно заданное вторичное правило сравнения имён, оставив бизнес-идентификатор последним критерием.
Отсутствующие имена нужно единообразно обрабатывать на этапе преобразования модели: например, заменять пустой строкой или предусмотреть ветку «элементы без имени в конце». Нельзя произвольно подставлять значения по умолчанию внутри компаратора. Для сортировки по убыванию тоже нужно заново определить направление сравнения ключей, а не просто инвертировать результат сравнения по возрастанию: такая инверсия заставит считать равные объекты «меньшими».
Запустите тесты в облаке и сохраните данные о сбоях
В тестовом задании на облачном Mac зафиксируйте Xcode, среду выполнения симулятора и локаль сравнения. Запустите эти тесты через существующую точку запуска модульных тестов. Сохраните идентификатор коммита, версию набора инструментов, входные данные сбойного теста и фактическую последовательность идентификаторов на выходе. Настоящие имена клиентских файлов записывать не нужно.
Сначала проверьте чистую функцию, затем интерфейс: обновите список два раза подряд и убедитесь, что элементы с одинаковыми именами не меняются местами. Удалите один элемент и вставьте его снова, проверив, что относительный порядок остальных соответствует правилам. Слой интерфейса должен использовать тот же компаратор, а не отдельную реализацию сортировки.
Фиксированная локаль не гарантирует, что системные правила сортировки никогда не изменятся. После обновления среды выполнения повторно проверьте репрезентативные примеры на разных языках. Если различия вызваны системными правилами, решите, принять ли изменение или перейти на собственные ключи сортировки, вместо того чтобы просто обновить все снимки.
Итоговый результат должен включать контракт сортировки, репрезентативные примеры, проверки свойств компаратора и результаты проверки обновления списка. Тогда при следующем изменении порядка можно будет определить, что изменилось — данные, правила или среда выполнения, — а не снова гадать по скриншотам.
Часто задаваемые вопросы
Достаточно ли стабильного алгоритма сортировки?
Нет. Он сохраняет относительный порядок равных элементов во входных данных. Если этот порядок меняется, нужен постоянный уникальный идентификатор как последний ключ сравнения.
Фиксированная локаль делает снимок порядка вечным?
Нет. Правила сравнения строк могут измениться вместе с системной средой. При её обновлении повторно проверяйте характерные примеры и свойства компаратора.
Арендуйте облачный Mac для следующей сборки
Выберите модель, город и срок аренды, чтобы работать со своими инструментами разработки на выделенном физическом узле. Доступность проверяйте по актуальным данным в консоли.