Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 6 из 25 ~18 мин +50 XP

ElementsMatch, Subset и Len: коллекции под контролем

Проверка слайсов через assert.Equal ломается на порядке: API отдаёт те же записи в другом порядке, и тест красный при верных данных. ElementsMatch в testify сравнивает содержимое без оглядки на порядок, Subset проверяет подмножество, Len — объём. Разберём каждую, кратность элементов, слайсы структур и ловушку nil против пустого.

Содержание урока · 10 разделов

Тест каталога краснел после каждого релиза: записи те же, но SQL-план поменялся и выгрузка приходит в другом порядке. assert.Equal честен — он сверяет по индексам, как разобрано в уроке про сравнение структур и слайсов, и для него [«a», «b»] и [«b», «a»] разные слайсы. Вопрос в другом: что проверяет сам тест? Если состав выгрузки — то Equal проверяет лишнее. Проверка слайсов в testify разложена по намерениям: Equal для порядка, ElementsMatch для состава, Subset для обязательных элементов, Len для объёма, Contains для точечных проверок. Отдельно — случай, когда порядок всё-таки часть контракта и как его не проглядеть. Здесь разберём их на реальном коде, посмотрим, как ElementsMatch считает кратность, сравним слайсы структур и разберём ловушку, на которой молча расходятся контракт API и его тесты: nil против пустого слайса.

ElementsMatch: те же элементы в любом порядке#

assert.ElementsMatch(t, listA, listB) проверяет, что оба аргумента — массив или слайс — содержат одни и те же элементы, порядок не важен, а кратность учитывается: если единица встречается в первом списке дважды, во втором она обязана встретиться дважды же. Документация формулирует прямо: при дубликатах число вхождений каждого элемента в обоих списках должно совпасть. Под капотом — diffLists: для каждого элемента A ищется ещё не занятый равный элемент B через ObjectsAreEqual, повторения считаются поэкземплярно, а непарные остатки попадают в отчёт как лишние и недостающие. Отчёт называется elements differ и перечисляет обе стороны — видно, кто именно потерялся, а не только факт провала. Две границы применимости. Аргументами могут быть только массив и слайс: карту ElementsMatch не примет и вежливо скажет expecting array or slice. И элементы сверяются через ObjectsAreEqual — все ловушки reflect.DeepEqual переносятся на каждый элемент списка: time.Time внутри структуры повторит свои фокусы.

ElementsMatch на выгрузке из базы

GO
func TestCatalog(t *testing.T) {
        got := loadCatalog() // порядок строк не гарантирован

        want := []Item{
                {SKU: "book-1", Price: 500},
                {SKU: "book-2", Price: 700},
                {SKU: "pen-9", Price: 90},
        }

        assert.ElementsMatch(t, want, got)
}

Тест фиксирует состав выгрузки и не парится о порядке: БД вольна возвращать строки как угодно. Если запись потерялась или задублировалась, отчёт elements differ перечислит лишние и недостающие SKU. Полный прогон — локальным go test: тестовые пакеты песочница не исполняет.

Кратность — то, чем ElementsMatch отличается от «множества на глаз». Пары [«a», «a», «b»] и [«a», «b», «b»] наборы значений совпадают, а тест красный: лишняя единица не нашла пары, и отчёт покажет extra с обеих сторон. Поэкземплярный подсчёт защищает от тихих дублей в выгрузках: когда запрос начал возвращать строку дважды, тест на состав упадёт, хотя «набор» не изменился. Пустые списки свёрнуты в отдельный случай: если оба аргумента пусты — длины ноль, включая nil-слайсы — ElementsMatch проходит. Это контрастирует с Equal, который различает nil и пустое; о цене контраста — в danger-блоке ниже. Когда тест красный, а списки на вид совпадают, распечатайте обе стороны через fmt.Sprintf("%#v", x): непарный элемент обычно дубликат с невидимой разницей или значение другого типа.

Subset и Len: граница и объём#

assert.Subset(t, list, subset) проверяет, что каждый элемент subset присутствует в list — порядок и лишние элементы списка не мешают. Кратность Subset не сверяет: повтор в подмножестве требует лишь, чтобы элемент встретился хотя бы раз. Нюанс: nil в роли подмножества проходит на любом списке — testify считает nil пустым множеством. list может быть массивом, слайсом или картой — на карте ищутся ключи, а когда и список, и подмножество карты, сверяются ещё и значения по общим ключам. assert.Len(t, obj, n) требует точную длину и работает со всем, что берёт builtin len: слайс, массив, карта, строка, канал, указатель на них. На неподходящем типе отчёт скажет could not be applied builtin len(), на неверной длине — should have N item(s), but has M. Отдельный случай: на типизированном nil-слайсе Len честно вернёт ноль, а на нетипизированном nil откажется с тем же could not be applied builtin len() — слайс, даже пустой, обязан быть слайсом. Пара Len и Subset описывает форму ответа без перечисления всего содержимого: размер страницы фиксирован, обязательные разделы на месте.

Subset и Len на ответе API

GO
func TestFeed(t *testing.T) {
        got := fetchFeed() // []string — активные разделы

        assert.Len(t, got, 4)                          // ровно четыре
        assert.Subset(t, got, []string{"news", "ads"}) // оба обязаны быть
        assert.Contains(t, got, "blog")                // и blog на месте
}

Len держит объём: если фид разросся на пятый раздел, тест упадёт — и это сигнал пересмотреть контракт, а не заглушить проверку. Subset гарантирует присутствие news и ads в любом порядке, Contains добивает точечную проверку. fetchFeed ходит в сеть, поэтому тест выполняется на вашей машине — в браузерной песочнице сети нет.

Слайс структур через ElementsMatch#

Слайсы структур — главный рабочий случай ElementsMatch: выгрузки заказов, корзины, подписки. Элементы сверяются через ObjectsAreEqual, то есть reflect.DeepEqual по каждому, — и все ловушки прошлого урока повторяются поэлементно. time.Time в полях структур красит тест на равных слайсах: снимайте монотонные метки и выравнивайте зоны ещё в фикстурах. unexported-поля участвуют в сверке — приватный кэш в элементе сделает «одинаковые» списки разными. С указателями спокойнее, чем кажется: DeepEqual разыменовывает указатели и сравнивает значения, поэтому []*Order из разных адресов с равным содержимым пройдёт. Красным будет пара nil-элемент против указателя на нулевую структуру — внутреннее представление различается. Практическое правило: чем больше в элементах непрозрачных типов, тем ближе момент, когда тест на состав стоит разбить на Len плюс сверку ключевых полей по отдельности.

Дубликаты в корзине

GO
func TestBasket(t *testing.T) {
        got := basketItems() // []*Item в произвольном порядке

        want := []*Item{
                {SKU: "book-1", Qty: 2},
                {SKU: "book-1", Qty: 2}, // дважды: кратность важна
                {SKU: "pen-9", Qty: 1},
        }

        assert.ElementsMatch(t, want, got)
}

Кратность работает и на структурах: каждая из двух одинаковых позиций занимает свой экземпляр в сверке. Если вторая book-1 потерялась, отчёт elements differ покажет её в недостающих. Корзина не обещает порядка позиций, зато обещает состав и количество — ровно то, что сверяет ElementsMatch.

Contains: строка, карта, слайс#

assert.Contains универсален до хитрости: поведение зависит от типа контейнера. Строка — ищется подстрока, внутри работает strings.Contains: Contains(t, "catalog", "alog") зелёный. Слайс и массив — ищется элемент через ObjectsAreEqual, включая структуры. Карта — ищется КЛЮЧ через MapIndex, значения карты Contains не проверяет никогда. Отсюда классическая ошибка: Contains(t, map[string]string{"a": "x"}, "x") краснеет — среди ключей «x» нет. Для значений достаньте их в слайс или сравните карту целиком через Equal. NotContains зеркален на всех трёх ветках: NotContains(t, m, "deleted") зелёный, пока ключ deleted не появился, — удобный сторож против протёкших внутренних статусов в публичном ответе. В связке с Len и Subset Contains закрывает быстрый контроль формы ответа: объём известен, обязательные элементы на месте, запрещённого нет.

Пять проверок коллекций#

ПроверкаЧто гарантируетТипичное место
assert.Equalтот же состав в том же порядкесортировка, топ-N, сериализация
assert.ElementsMatchтот же состав и кратность, порядок любойвыгрузки из БД, неупорядоченные ответы
assert.Subsetкаждый элемент подмножества присутствуетобязательные разделы в ответе
assert.Lenточное число элементовпагинация, размер страницы
assert.Containsэлемент слайса, ключ карты, подстрокаточечная проверка без перечисления

Какую проверку слайса выбрать#

  • Порядок значим — assert.Equal: сверка по индексам, дифф покажет первое расхождение.
  • Порядок не значим, состав и кратность значимы — assert.ElementsMatch.
  • Нужно только присутствие обязательных элементов — assert.Subset, кратность он не трогает.
  • Нужен только объём — assert.Len: дешёвая проверка формы ответа.
  • Один элемент, ключ или подстрока — assert.Contains.
  • nil и пустое различны для Equal и совпадают для ElementsMatch и Len(t, x, 0): нормализуйте слайс на границе пакета.

Проверка: кратность

assert.ElementsMatch(t, []int{1, 1, 2}, []int{1, 2, 2}) — что произойдёт?

Проверка: nil и пустое

got — nil-слайс []Item(nil). Какие проверки пройдут?

Что покажет прогон?

Прочитайте код и выберите, что он выведет:

func TestFeed(t *testing.T) {
        got := []string{"b", "a", "c"}

        assert.Equal(t, []string{"a", "b", "c"}, got)
        assert.ElementsMatch(t, []string{"a", "b", "c"}, got)
}

Соберите контроль фида

Допишите три проверки: точный объём, обязательные элементы и один точечный элемент.

func TestFeed(t *testing.T) { got := fetchFeed() assert.(t, got, 3) // ровно три записи assert.(t, got, []string{"news", "blog"}) // обе обязаны быть assert.(t, got, "docs") // и docs на месте }

Соберите тест на состав каталога

Расставьте строки: загрузка, эталон, проверка состава и контроль объёма.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: коллекции под контролем

Термин

assert.ElementsMatch

нажмите, чтобы перевернуть
Определение

Те же элементы в любом порядке, кратность поэкземплярно; аргументы — только массив или слайс; два пустых списка, включая nil, проходят.

1 / 6

Что дальше#

Состав, порядок и объём под контролем — остаются числа, где Equal подводит уже по другой причине: 0.1+0.2 не равно 0.3, и тест на метрики краснеет без вины кода. Урок про InDelta и InEpsilon разбирает дельты и допуски. Дальше — простые проверки на каждый день: True, False, Nil и Empty, где пустота слайса превращается из ловушки в отдельную проверку. Примеры урока прогоняются локальным go test, как и весь курс: песочница исполняет фрагменты логики, но тестовые пакеты с зависимостью testify требуют терминала. Карта всех двадцати пяти тем — на странице курса по testify.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • ElementsMatch сверяет состав без порядка: те же элементы с той же кратностью, любые индексы.
  • Кратность считается поэкземплярно: лишний дубль или потерянная запись красят тест, совпадение набора значений не спасает.
  • Subset гарантирует присутствие каждого элемента подмножества и не следит за кратностью; nil-подмножество проходит всегда.
  • Len требует точную длину и работает со всем, что берёт builtin len: слайс, массив, карта, строка, канал.
  • Contains ищет подстроку в строке, элемент в слайсе и ключ в карте — значения карты остаются невидимы.
  • Порядок не значим — ElementsMatch; порядок значим — Equal: зелёный тест рейтинга на ElementsMatch — брак проверки.
  • nil и пустой слайс равны для ElementsMatch и Len и различны для Equal: нормализуйте форму на границе пакета.

Домашнее задание

Добавьте в свой тестовый пакет две проверки одной выгрузки: Equal на отсортированный слайс и ElementsMatch на несортированный. Затем разверните в JSON сначала [] и null — и посмотрите, какая проверка различает nil и пустое. Нормализуйте слайс на границе пакета и прогоните go test локально.

Личные заметкиПусто
Хранятся только в вашем браузере (Testify · ElementsMatch, Subset и Len: коллекции под контролем)

Вопросы и ответы по уроку

ElementsMatch проверяет порядок элементов?

Нет: он парит элементы независимо от индексов и сверяет кратность поэкземплярно. Порядок для него не существует. Если последовательность — часть контракта, как у топ-N, сортировки или очереди вывода, пишите assert.Equal: он сравнивает по индексам и покажет первое расхождение.

Чем Subset отличается от ElementsMatch?

ElementsMatch требует полного совпадения состава с обеих сторон: лишние элементы в списке красят тест. Subset проверяет одно направление — каждый элемент подмножества обязан найтись в списке, а лишнее в списке допустимо. Кратность Subset тоже не сверяет: повтор в подмножестве требует лишь одного вхождения.

Почему ElementsMatch зелёный на nil и пустом слайсе, а Equal красный?

ElementsMatch сворачивает случай двух пустых списков заранее: нулевые длины, включая nil, считаются совпадающими. Equal уходит в reflect.DeepEqual, который различает внутреннее представление nil-слайса и пустого. Если форма JSON-ответа — контракт, нормализуйте слайс на границе пакета и зафиксируйте её отдельным тестом.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Viper25 уроков~9 чпрактика25 уроков уже доступны

Viper: конфигурация приложений на Go

Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.

  • Собирать конфиг из файлов, окружения и флагов в один реестр
  • Читать значения типизированно: Get, GetString, Unmarshal в структуры
  • Задавать приоритеты источников: флаг > env > файл > default
  • Перечитывать конфиг на лету через WatchConfig и OnConfigChange